diff --git a/Pipfile b/Pipfile index 6e5842566..94b2998bf 100644 --- a/Pipfile +++ b/Pipfile @@ -18,6 +18,8 @@ openpyxl = "*" pyparsing = "==2.4.7" networkx = "*" pandas = "*" +xlrd = "*" +regex = "*" [dev-packages] mkdocs = "*" diff --git a/Pipfile.lock b/Pipfile.lock index c6f41514f..e6797871d 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "e5cec93e6913befc4ebfc9825babe72541f016aa05e3f88b6d87d9af03a662fc" + "sha256": "dba16a0708756d54af2eb8e061285afcc94535f8e8d073b744759e8bfd4834f5" }, "pipfile-spec": 6, "requires": { @@ -37,7 +37,7 @@ "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d", "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==2022.6.15" }, "charset-normalizer": { @@ -45,7 +45,7 @@ "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==2.1.1" }, "click": { @@ -69,7 +69,7 @@ "sha256:8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c", "sha256:a2ba85d1d6a74ef63837eed693bcb89c3f752169b0e3e7ae5b16ca5e1b3deada" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==1.1.0" }, "idna": { @@ -182,11 +182,11 @@ }, "networkx": { "hashes": [ - "sha256:15a7b81a360791c458c55a417418ea136c13378cfdc06a2dcdc12bd2f9cf09c1", - "sha256:a762f4b385692d9c3a6f2912d058d76d29a827deaedf9e63ed14d397b8030687" + "sha256:2a30822761f34d56b9a370d96a4bf4827a535f5591a4078a453425caeba0c5bb", + "sha256:bd2b7730300860cbd2dafe8e5af89ff5c9a65c3975b352799d87a6238b4301a6" ], "index": "pypi", - "version": "==2.8.5" + "version": "==2.8.6" }, "numpy": { "hashes": [ @@ -330,6 +330,86 @@ "index": "pypi", "version": "==6.2.0" }, + "regex": { + "hashes": [ + "sha256:02b6dc102123f5178796dcdb5a90f6e88895607fd1a1d115d8de1af8161ca2b4", + "sha256:0843cc977b9cc00eb2299b624db6481d25e7f5b093f7a7c2bb727028d4a26eda", + "sha256:085ca3dc9360c0210e0a70e5d34d66454a06077644e7679fef6358b1f053e62e", + "sha256:0a9d5a64e974bc5f160f30f76aaf993d49eeddb405676be6bf76a5a2c131e185", + "sha256:0de0ce11c0835e1117eacbfe8fa6fa98dc0e8e746b486735cb0fdebe46a02222", + "sha256:1418d3506a9582b23a27373f125ea2b0da523c581e7cf678a6f036254d134faa", + "sha256:14750172c0a616140a8f496dfef28ed24080e87d06d5838e008f959ad307a8c5", + "sha256:1b6d2c579ffdcbb3d93f63b6a7f697364594e1c1b6856958b3e61e3ca22c140a", + "sha256:1df31eaf147ecff3665ba861acb8f78221cd5501df072c9151dfa341dd24599f", + "sha256:21b6f939916aa61beea56393ebc8a9999060632ac22b8193c2cb67d6fd7cb2c3", + "sha256:2240fce3af236e4586a045c1be8bbf16c4f8831e68b7df918b72fc31a80143be", + "sha256:242f546fc5e49bb7395624ac3b4fc168bf454e11ace9804c58c4c3a90d84e38f", + "sha256:25bffa248b99b53a61b1f20fc7d19f711e38e9f0bc90d44c26670f8dc282ad7d", + "sha256:2ada67e02fa3fcca9e3b90cf24c2c6bc77f0abc126209937956aea10eeba40c7", + "sha256:2c198921afc811bc0f105c6e5150fbdebf9520c9b7d43cfc0ab156ca97f506d7", + "sha256:370b1d7aed26e29915c3fb3e72e327f194824a76cedb60c0b9f6c6af53e89d72", + "sha256:3aafbbf5076f2a48bcf31ceb42b410323daaa0ddb42544640592957bc906ace6", + "sha256:3d3d769b3d485b28d6a591b46723dbacc696e6503f48a3ef52e6fc2c90edb482", + "sha256:3d83fd6dd4263595d0e4f595d4abd54397cbed52c0147f7dd148a7b72910301e", + "sha256:45cb798095b886e4df6ff4a1f7661eb70620ccdef127e3c3e00a1aaa22d30e53", + "sha256:4bd9443f7ff6e6288dd4496215c5d903f851e55cbc09d5963587af0c6d565a0a", + "sha256:4bdfd016ab12c4075ef93f025b3cf4c8962b9b7a5e52bb7039ab64cb7755930c", + "sha256:4c6554073e3e554fbb3dff88376ada3da32ca789ea1b9e381f684d49ddb61199", + "sha256:4dad9d68574e93e1e23be53b4ecfb0f083bd5cc08cc7f1984a4ee3ebf12aa446", + "sha256:4e12a3c2d4781ee5d03f229c940934fa1e4ea4f4995e68ab97a2815b139e0804", + "sha256:53c9eca0d6070a8a3de42182ad26daf90ba12132eb74a2f45702332762aff84e", + "sha256:5910bb355f9517309f77101238dbacb7151ede3434a2f1fad26ecc62f13d8324", + "sha256:5c77eab46f3a2b2cd8bbe06467df783543bf7396df431eb4a144cc4b89e9fb3c", + "sha256:5d541bc430a74c787684d1ebcd205a5212a88c3de73848143e77489b2c25b911", + "sha256:5e7c8f9f8824143c219dd93cdc733c20d2c12f154034c89bcb4911db8e45bd92", + "sha256:5f14430535645712f546f1e07013507d1cc0c8abd851811dacce8c7fb584bf52", + "sha256:6059ae91667932d256d9dc03abd3512ebcade322b3a42d1b8354bd1db7f66dcc", + "sha256:61f6966371fa1cbf26c6209771a02bef80336cdaca0c0af4dfa33d51019c0b93", + "sha256:62d56a9d3c1e5a83076db4da060dad7ea35ac2f3cbd3c53ba5a51fe0caedb500", + "sha256:634f090a388351eadf1dcc1d168a190718fb68efb4b8fdc1b119cf837ca01905", + "sha256:64ecfcc386420192fbe98fdde777d993f7f2dfec9552e4f4024d3447d3a3e637", + "sha256:6af38997f178889d417851bae8fb5c00448f7405cfcab38734d771f1dd5d5973", + "sha256:6b30c8d299ba48ee919064628fd8bc296bdc6e4827d315491bea39437130d3e1", + "sha256:6f0c8807bac16984901c0573725bad786f2f004f9bd5df8476c6431097b6c5b3", + "sha256:6f62c8a59f6b8e608880c61b138ae22668184bc266b025d33200dcf2cebe0872", + "sha256:74d4aabd612d32282f3cb3ebb4436046fb840d25c754157a755bc9f66e7cd307", + "sha256:7658d2dfc1dabfb008ffe12ae47b98559e2aedd8237bee12f5aafb74d90479e3", + "sha256:777ceea2860a48e9e362a4e2a9a691782ea97bd05c24627c92e876fdd2c22e61", + "sha256:79f34d5833cd0d53ecf48bc030e4da3216bd4846224d17eeb64509be5cb098fd", + "sha256:7a52d547259495a53e61e37ffc6d5cecf8d298aeb1bc0d9b25289d65ddb31183", + "sha256:840063aa8eeb1dda07d7d7dee15648838bffef1d415f5f79061854a182a429aa", + "sha256:8e8ec94d1b1a0a297c2c69a0bf000baf9a79607ca0c084f577f811a9b447c319", + "sha256:95fb62a3980cf43e76c2fe95edab06ec70dc495b8aa660975eb9f0b2ffdae1e1", + "sha256:9668da78bcc219542467f51c2cd01894222be6aceec4b5efb806705900b794d8", + "sha256:99a7c5786de9e92ff5ffee2e8bed745f5d25495206f3f14656c379031e518334", + "sha256:a1e283ad918df44bad3ccf042c2fe283c63d17617570eb91b8c370ef677b0b83", + "sha256:a25d251546acb5edb1635631c4ae0e330fa4ec7c6316c01d256728fbfb9bbff2", + "sha256:abe1adb32e2535aaa171e8b2b2d3f083f863c9974a3e6e7dae6bf4827fc8b983", + "sha256:ae85112da2d826b65aa7c7369c56ca41d9a89644312172979cbee5cf788e0b09", + "sha256:b3379a83dc63fe06538c751961f9ed730b5d7f08f96a57bbad8d52db5820df1f", + "sha256:b3c7c6c4aac19b964c1d12784aecae7f0315314640b0f41dd6f0d4e2bf439072", + "sha256:b7ddecc80e87acf12c2cf12bf3721def47188c403f04e706f104b5e71fed2f31", + "sha256:bbaf6785d3f1cd3e617b9d0fb3c5528023ef7bc7cc1356234801dc1941df8ce9", + "sha256:be6f5b453f7ed2219a9555bb6840663950b9ab1dc034216f68eac64db66633c2", + "sha256:c2b6404631b22617b5127c6de2355393ccda693ca733a098b6802e7dabb3457a", + "sha256:c4f6609f6e867a58cdf173e1cbe1f3736d25962108bd5cb01ad5a130875ff2c8", + "sha256:c76dd2c0615a28de21c97f9f6862e84faef58ff4d700196b4e395ef6a52291e4", + "sha256:c78c72f7878071a78337510ec78ab856d60b4bdcd3a95fd68b939e7cb30434b3", + "sha256:cb0c9a1476d279524538ba9a00ecec9eadcef31a6a60b2c8bd2f29f62044a559", + "sha256:ccb986e80674c929f198464bce55e995178dea26833421e2479ff04a6956afac", + "sha256:cfa62063c5eafb04e4435459ce15746b4ae6c14efeae8f16bd0e3d2895dad698", + "sha256:d13bd83284b46c304eb10de93f8a3f2c80361f91f4e8a4e1273caf83e16c4409", + "sha256:d76e585368388d99ddd2f95989e6ac80a8fe23115e93931faad99fa34550612f", + "sha256:dc32029b9cc784a529f9201289d4f841cc24a2ae3126a112cd467bc41bbc2f10", + "sha256:e0b55651db770b4b5a6c7d015f24d1a6ede307296bbdf0c47fc5f6a6adc7abee", + "sha256:e37886929ee83a5fa5c73164abada00e7f3cc1cbf3f8f6e1e8cfecae9d6cfc47", + "sha256:f7b88bc7306136b123fd1a9beed16ca02900ee31d1c36e73fa33d9e525a5562d", + "sha256:fac611bde2609a46fcbd92da7171286faa2f5c191f84d22f61cd7dc27213f51d", + "sha256:fafed60103132e74cdfbd651abe94801eb87a9765ce275b3dca9af8f3e06622a" + ], + "index": "pypi", + "version": "==2022.8.17" + }, "requests": { "hashes": [ "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", @@ -348,11 +428,11 @@ }, "setuptools": { "hashes": [ - "sha256:7f4bc85450898a09f76ebf28b72fa25bc7111f6c7d665d514a60bba9c75ef2a9", - "sha256:a3ca5857c89f82f5c9410e8508cb32f4872a3bafd4aa7ae122a24ca33bccc750" + "sha256:2e24e0bec025f035a2e72cdd1961119f557d78ad331bb00ff82efb2ab8da8e82", + "sha256:7732871f4f7fa58fb6bdcaeadb0161b2bd046c85905dbaa066bdcbcc81953b57" ], "markers": "python_version >= '3.7'", - "version": "==65.2.0" + "version": "==65.3.0" }, "six": { "hashes": [ @@ -364,11 +444,11 @@ }, "urllib3": { "hashes": [ - "sha256:c33ccba33c819596124764c23a97d25f32b28433ba0dedeb77d873a38722c9bc", - "sha256:ea6e8fb210b19d950fab93b60c9009226c63a28808bc8386e05301e25883ac0a" + "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", + "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'", - "version": "==1.26.11" + "version": "==1.26.12" }, "validators": { "hashes": [ @@ -376,6 +456,14 @@ ], "index": "pypi", "version": "==0.20.0" + }, + "xlrd": { + "hashes": [ + "sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd", + "sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88" + ], + "index": "pypi", + "version": "==2.0.1" } }, "develop": { @@ -413,7 +501,7 @@ "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d", "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==2022.6.15" }, "chardet": { @@ -429,7 +517,7 @@ "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==2.1.1" }, "click": { @@ -450,10 +538,10 @@ }, "distlib": { "hashes": [ - "sha256:a7f75737c70be3b25e2bee06288cec4e4c221de18455b2dd037fe2a795cab2fe", - "sha256:b710088c59f06338ca514800ad795a132da19fda270e3ce4affc74abf955a26c" + "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46", + "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e" ], - "version": "==0.3.5" + "version": "==0.3.6" }, "ghp-import": { "hashes": [ @@ -573,11 +661,11 @@ }, "mkdocs-material": { "hashes": [ - "sha256:319a6254819ce9d864ff79de48c43842fccfdebb43e4e6820eef75216f8cfb0a", - "sha256:92c70f94b2e1f8a05d9e05eec1c7af9dffc516802d69222329db89503c97b4f3" + "sha256:166287bb0e4197804906bf0389a852d5ced43182c30127ac8b48a4e497ecd7e5", + "sha256:704c64c3fff126a3923c2961d95f26b19be621342a6a4e49ed039f0bb7a5c540" ], "index": "pypi", - "version": "==8.4.1" + "version": "==8.4.2" }, "mkdocs-material-extensions": { "hashes": [ @@ -824,11 +912,11 @@ }, "setuptools": { "hashes": [ - "sha256:7f4bc85450898a09f76ebf28b72fa25bc7111f6c7d665d514a60bba9c75ef2a9", - "sha256:a3ca5857c89f82f5c9410e8508cb32f4872a3bafd4aa7ae122a24ca33bccc750" + "sha256:2e24e0bec025f035a2e72cdd1961119f557d78ad331bb00ff82efb2ab8da8e82", + "sha256:7732871f4f7fa58fb6bdcaeadb0161b2bd046c85905dbaa066bdcbcc81953b57" ], "markers": "python_version >= '3.7'", - "version": "==65.2.0" + "version": "==65.3.0" }, "six": { "hashes": [ @@ -851,7 +939,7 @@ "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" ], - "markers": "python_version < '3.11'", + "markers": "python_version >= '3.7'", "version": "==2.0.1" }, "tomlkit": { @@ -887,19 +975,19 @@ }, "urllib3": { "hashes": [ - "sha256:c33ccba33c819596124764c23a97d25f32b28433ba0dedeb77d873a38722c9bc", - "sha256:ea6e8fb210b19d950fab93b60c9009226c63a28808bc8386e05301e25883ac0a" + "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", + "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'", - "version": "==1.26.11" + "version": "==1.26.12" }, "vistir": { "hashes": [ - "sha256:6506888420ce1842f70c314739ad9853eb5823d195b5bec4a16251d172a48fc4", - "sha256:66e56f31ede7181bef1406a394b07c7b4db9910698c8269d26a12815c8e3ccf0" + "sha256:1a89a612fb667c26ed6b4ed415b01e0261e13200a350c43d1990ace0ef44d35b", + "sha256:a8beb7643d07779cdda3941a08dad77d48de94883dbd3cb2b9b5ecb7eb7c0994" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.5.6" + "markers": "python_version not in '3.0, 3.1, 3.2, 3.3' and python_version >= '3.7'", + "version": "==0.6.1" }, "watchdog": { "hashes": [ diff --git a/dev-requirements.txt b/dev-requirements.txt index 29e5f36af..f0021afc7 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -3,12 +3,12 @@ attrs~=22.1.0; python_version >= '3.5' autopep8~=1.7.0 cached-property~=1.5.2 cerberus~=1.3.4 -certifi~=2022.6.15; python_full_version >= '3.6.0' +certifi~=2022.6.15; python_version >= '3.6' chardet~=5.0.0; python_version >= '3.6' -charset-normalizer~=2.1.1; python_full_version >= '3.6.0' +charset-normalizer~=2.1.1; python_version >= '3.6' click~=8.1.3 colorama~=0.4.5; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' -distlib~=0.3.5 +distlib~=0.3.6 ghp-import~=2.1.0 idna~=3.3; python_version >= '3.5' importlib-metadata~=4.12.0; python_version >= '3.7' @@ -19,7 +19,7 @@ markupsafe~=2.1.1; python_version >= '3.7' mergedeep~=1.3.4; python_version >= '3.6' mkdocs~=1.3.1 mkdocs-include-markdown-plugin~=3.6.1 -mkdocs-material~=8.4.1 +mkdocs-material~=8.4.2 mkdocs-material-extensions~=1.0.3; python_version >= '3.6' mypy~=0.971 mypy-extensions~=0.4.3 @@ -44,16 +44,16 @@ pyyaml~=6.0; python_version >= '3.6' pyyaml-env-tag~=0.1; python_version >= '3.6' requests~=2.28.1 requirementslib~=1.6.9; python_version >= '3.7' -setuptools~=65.2.0; python_version >= '3.7' +setuptools~=65.3.0; python_version >= '3.7' six~=1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' toml~=0.10.2; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' -tomli~=2.0.1; python_version < '3.11' +tomli~=2.0.1; python_version >= '3.7' tomlkit~=0.11.4; python_version >= '3.6' and python_version < '4.0' types-requests~=2.28.9 types-urllib3~=1.26.23 typing-extensions~=4.3.0; python_version >= '3.7' -urllib3~=1.26.11; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4' -vistir~=0.5.6; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' +urllib3~=1.26.12; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4' +vistir~=0.6.1; python_version not in '3.0, 3.1, 3.2, 3.3' and python_version >= '3.7' watchdog~=2.1.9; python_version >= '3.6' wheel~=0.37.1 zipp~=3.8.1; python_version >= '3.7' diff --git a/docs/assets/images/img-excel2xml.png b/docs/assets/images/img-excel2xml.png new file mode 100644 index 000000000..45840ea30 Binary files /dev/null and b/docs/assets/images/img-excel2xml.png differ diff --git a/docs/assets/templates/excel2xml_sample_data.csv b/docs/assets/templates/excel2xml_sample_data.csv new file mode 100644 index 000000000..c199d22d0 --- /dev/null +++ b/docs/assets/templates/excel2xml_sample_data.csv @@ -0,0 +1,10 @@ +Resource identifier,Resource name,Long text,Image,Category,Complete?,Color,Date discovered,Exact time,Weight (kg),Find location,Number of descendants,Similar to,See also +res_0,First Resource,"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",testdata/bitstreams/test.jpg,"Mamals, Insects",yes,#00ff66,01.01.01,2019-10-23T13:45:12Z,,2761369,0,res_1,http://test.org +res_1,Second Resource,"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",,Birds,No,#ff00ff,2015_01_01,2019-10-23T13:45:12Z,45.8,2761369,12,res_8, +res_2,Third Resource,"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",testdata/bitstreams/test.jpg, Reptilles ,,,05.11.21,,,,3,, +res_3,Fourth Resource,"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",,Plants,0,,1.12.1973 - 6.1.1974,,,,2,, +res_4,Fifth Resource,"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",,Physics,TRUE,,"March 5,1908",,,,1,, +res_5,Sixth Resource,"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",,"Humans, Animals",FALSE,,1886/7,2009-10-10T12:00:00-05:00,200.382,,3,, +res_6,Seventh Resource,"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",,Amphibians,None,,1849/1850,,,,99,, +res_7,Eighth Resource,"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",testdata/bitstreams/test.jpg,Physics,,#ff00ff,01.01.01,,,,6,, +res_8,Ninth Resource,"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",,Plants,1,,26.2.-24.3.1948,,,,7,, \ No newline at end of file diff --git a/docs/assets/templates/excel2xml_sample_onto.json b/docs/assets/templates/excel2xml_sample_onto.json new file mode 100644 index 000000000..e35bc4802 --- /dev/null +++ b/docs/assets/templates/excel2xml_sample_onto.json @@ -0,0 +1,1526 @@ +{ + "prefixes": { + "dcterms": "http://purl.org/dc/terms/" + }, + "$schema": "https://raw.githubusercontent.com/dasch-swiss/dsp-tools/main/knora/dsplib/schemas/ontology.json", + "project": { + "shortcode": "082E", + "shortname": "rosetta", + "longname": "Rosetta: DSP example project", + "descriptions": { + "de": "Rosetta ist das Beispielprojekt für die DaSCH Service Platform. Es soll einerseits die Möglichkeiten illustrieren, die die Plattform aktuell bietet, andererseits aber auch intern aufzeigen, wo noch Verbesserungsbedarf besteht.", + "fr": "Rosetta est le projet exemplaire de la DaSCH Service Platform. D'une part, il vise à illustrer les possibilités actuellement offertes par la plate-forme, mais d'autre part, il montre également en interne les domaines où il est encore possible d'apporter des améliorations.", + "en": "Rosetta is the sample project for the DaSCH Service Platform. On one hand, it is intended to illustrate the possibilities currently offered by the platform, but on the other hand, it also shows internally where there is still room for improvement." + }, + "keywords": [ + "Textquellen", + "Objekte", + "Bilder", + "Audio", + "Kyrillisch", + "Griechisch", + "Keilschrift", + "Hieroglyphen", + "Hebräisch", + "Arabisch", + "Japanisch", + "Sonderzeichen", + "XML", + "Markup", + "Annotation", + "Texteigenschaften", + "textual sources", + "objects", + "images", + "audio", + "Cyrillic", + "Greek", + "cuneiform", + "hieroglyphs", + "Hebrew", + "Arabic", + "Japanese", + "special characters", + "textual properties", + "sources", + "objets", + "Cyrillique", + "Grec", + "cunéiforme", + "hiéroglyphes", + "Hébreu", + "Arabe", + "Japonais", + "caractères spéciaux", + "propriétés de texte", + "Data and Service Center for the Humanities (DaSCH)" + ], + "lists": [ + { + "name": "flatlist", + "labels": { + "en": "Flat list" + }, + "comments": { + "en": "Flat list" + }, + "nodes": [ + { + "name": "first-node", + "labels": { + "en": "First node" + } + }, + { + "name": "second-node", + "labels": { + "en": "Second node" + } + } + ] + }, + { + "name": "category", + "labels": { + "de": "Kategorie", + "en": "Category", + "fr": "Catégorie" + }, + "comments": { + "en": "A list containing categories", + "de": "Die Liste enthält Kategorien" + }, + "nodes": [ + { + "name": "artwork", + "labels": { + "de": "Kunstwerk", + "en": "Artwork", + "fr": "Oeuvre d'art" + } + }, + { + "name": "vehicles", + "labels": { + "de": "Fahrzeuge", + "en": "Vehicles", + "fr": "Véhicules" + } + }, + { + "name": "nature", + "labels": { + "de": "Natur", + "en": "Nature", + "fr": "Nature" + }, + "nodes": [ + { + "name": "humans", + "labels": { + "de": "Menschen", + "en": "Humans", + "fr": "Humains" + } + }, + { + "name": "animals", + "labels": { + "de": "Tiere", + "en": "Animals", + "fr": "Animaux" + }, + "nodes": [ + { + "name": "mammals", + "labels": { + "de": "Säugetiere", + "en": "Mammals", + "fr": "Mammifères" + } + }, + { + "name": "insects", + "labels": { + "de": "Insekten", + "en": "Insects", + "fr": "Insectes" + } + }, + { + "name": "birds", + "labels": { + "de": "Vögel", + "en": "Birds", + "fr": "Oiseaux" + } + }, + { + "name": "amphibians", + "labels": { + "de": "Amphibien", + "en": "Amphibians", + "fr": "Amphibiens" + } + }, + { + "name": "reptiles", + "labels": { + "de": "Reptilien", + "en": "Reptiles", + "fr": "Reptiles" + } + } + ] + }, + { + "name": "plants", + "labels": { + "de": "Pflanzen", + "en": "Plants", + "fr": "Plantes" + } + }, + { + "name": "weather", + "labels": { + "de": "Wetter", + "en": "Weather", + "fr": "Météo" + } + }, + { + "name": "physics", + "labels": { + "de": "Physik", + "en": "Physics", + "fr": "Physique" + } + } + ] + } + ] + } + ], + "groups": [ + { + "name": "rosetta-editors", + "descriptions": { + "en": "Editors for the rosetta-project" + }, + "selfjoin": false, + "status": true + } + ], + "users": [ + { + "username": "rosettaedit", + "email": "rosettaedit@test.org", + "givenName": "rosetta-given", + "familyName": "rosetta-family", + "password": "rosetta1234", + "lang": "de", + "groups": [ + ":rosetta-editors" + ], + "projects": [ + ":admin" + ] + } + ], + "ontologies": [ + { + "name": "rosetta", + "label": "rosetta", + "properties": [ + { + "name": "hasTime", + "super": [ + "hasValue" + ], + "object": "TimeValue", + "labels": { + "de": "Zeit", + "en": "Time", + "fr": "Heure" + }, + "gui_element": "TimeStamp" + }, + { + "name": "hasTextMedium", + "super": [ + "hasLinkTo" + ], + "object": "StillImageRepresentation", + "labels": { + "de": "Textträger", + "en": "Text medium", + "fr": "Support de texte" + }, + "gui_element": "Searchbox", + "gui_attributes": { + "numprops": 1 + } + }, + { + "name": "hasImage", + "super": [ + "hasLinkTo" + ], + "object": ":Image2D", + "labels": { + "de": "Bild", + "en": "Image", + "fr": "Image" + }, + "gui_element": "Searchbox", + "gui_attributes": { + "numprops": 1 + } + }, + { + "name": "hasOriginalText", + "super": [ + "hasValue" + ], + "object": "TextValue", + "labels": { + "de": "Text", + "en": "Text", + "fr": "Texte" + }, + "gui_element": "Richtext" + }, + { + "name": "hasTranscription", + "super": [ + "hasValue" + ], + "object": "TextValue", + "labels": { + "de": "Transkription", + "en": "Transcription", + "fr": "Transcription" + }, + "gui_element": "Richtext" + }, + { + "name": "hasTransliteration", + "super": [ + "hasValue" + ], + "object": "TextValue", + "labels": { + "de": "Transliteration", + "en": "Transliteration", + "fr": "Translittération" + }, + "gui_element": "Richtext" + }, + { + "name": "hasTranslation", + "super": [ + "hasValue" + ], + "object": "TextValue", + "labels": { + "de": "Übersetzung", + "en": "Translation", + "fr": "Traduction" + }, + "gui_element": "Richtext" + }, + { + "name": "hasDescription", + "super": [ + "hasValue", + "dcterms:description" + ], + "object": "TextValue", + "labels": { + "de": "Beschreibung", + "en": "Description", + "fr": "Description" + }, + "gui_element": "Richtext" + }, + { + "name": "hasAuthor", + "super": [ + "hasLinkTo", + "dcterms:creator" + ], + "object": ":Person", + "labels": { + "de": "Autor", + "en": "Author", + "fr": "Auteur" + }, + "gui_element": "Searchbox", + "gui_attributes": { + "numprops": 1 + } + }, + { + "name": "hasName", + "super": [ + "hasValue" + ], + "object": "TextValue", + "labels": { + "de": "Name", + "en": "Name", + "fr": "Nom" + }, + "gui_element": "SimpleText", + "gui_attributes": { + "maxlength": 128, + "size": 32 + } + }, + { + "name": "hasTitle", + "super": [ + "hasValue", + "dcterms:title" + ], + "object": "TextValue", + "labels": { + "de": "Titel", + "en": "Title", + "fr": "Titre" + }, + "gui_element": "SimpleText", + "gui_attributes": { + "maxlength": 255, + "size": 80 + } + }, + { + "name": "hasDate", + "super": [ + "hasValue" + ], + "object": "DateValue", + "labels": { + "de": "Datierung", + "en": "Dating", + "fr": "Datation" + }, + "gui_element": "Date" + }, + { + "name": "hasFindspot", + "super": [ + "hasValue" + ], + "object": "GeonameValue", + "labels": { + "de": "Fundort", + "en": "Find spot", + "fr": "Gisement" + }, + "gui_element": "Geonames" + }, + { + "name": "hasLocation", + "super": [ + "hasValue" + ], + "object": "GeonameValue", + "labels": { + "de": "Ort", + "en": "Location", + "fr": "Lieu" + }, + "gui_element": "Geonames" + }, + { + "name": "hasBibliographicReference", + "super": [ + "hasValue" + ], + "object": "TextValue", + "labels": { + "de": "Literatur", + "en": "Literature", + "fr": "Littérature" + }, + "gui_element": "SimpleText", + "gui_attributes": { + "maxlength": 128, + "size": 128 + } + }, + { + "name": "hasExternalLink", + "super": [ + "hasValue" + ], + "object": "UriValue", + "labels": { + "de": "Externer Link", + "en": "External link", + "fr": "Lien hypertexte externe" + }, + "gui_element": "SimpleText", + "gui_attributes": { + "maxlength": 128, + "size": 128 + } + }, + { + "name": "hasIdentifier", + "super": [ + "hasValue" + ], + "object": "UriValue", + "labels": { + "de": "GND", + "en": "GND", + "fr": "GND" + }, + "gui_element": "SimpleText", + "gui_attributes": { + "maxlength": 128, + "size": 128 + } + }, + { + "name": "hasRelatedArtwork", + "super": [ + "hasLinkTo" + ], + "object": "Resource", + "labels": { + "de": "Verwandtes Werk", + "en": "Related Artwork", + "fr": "Oeuvre reliée" + }, + "gui_element": "Searchbox", + "gui_attributes": { + "numprops": 1 + } + }, + { + "name": "hasCreator", + "super": [ + "hasLinkTo", + "dcterms:creator" + ], + "object": ":Person", + "labels": { + "de": "Künstler", + "en": "Artist", + "fr": "Artiste" + }, + "gui_element": "Searchbox" + }, + { + "name": "inInstitution", + "super": [ + "hasLinkTo" + ], + "object": ":Institution", + "labels": { + "de": "Aufbewahrende Institution", + "en": "Custodian institution", + "fr": "Institution de garde" + }, + "gui_element": "Searchbox", + "gui_attributes": { + "numprops": 1 + } + }, + { + "name": "hasInventoryNumber", + "super": [ + "hasValue" + ], + "object": "TextValue", + "labels": { + "de": "Inventarnummer", + "en": "Inventory number", + "fr": "Numéro d'inventaire" + }, + "gui_element": "SimpleText", + "gui_attributes": { + "maxlength": 80, + "size": 25 + } + }, + { + "name": "hasPagenum", + "super": [ + "seqnum" + ], + "object": "IntValue", + "labels": { + "de": "Seitenzahl", + "en": "Page number", + "fr": "Numéro de page" + }, + "gui_element": "SimpleText", + "gui_attributes": { + "maxlength": 8, + "size": 8 + } + }, + { + "name": "partOf", + "super": [ + "isPartOf" + ], + "object": ":Book", + "labels": { + "de": "ist Teil von", + "en": "is part of", + "fr": "fait partie de" + }, + "gui_element": "Searchbox", + "gui_attributes": { + "numprops": 1 + } + }, + { + "name": "hasCategory", + "super": [ + "hasValue" + ], + "object": "ListValue", + "labels": { + "de": "Kategorie", + "en": "Category", + "fr": "Catégorie" + }, + "gui_element": "List", + "gui_attributes": { + "hlist": "category" + } + }, + { + "name": "hasFlatList", + "super": [ + "hasValue" + ], + "object": "ListValue", + "labels": { + "de": "Flatlist", + "en": "Flatlist", + "fr": "Flatlist" + }, + "gui_element": "Radio", + "gui_attributes": { + "hlist": "flatlist" + } + }, + { + "name": "hasColor", + "super": [ + "hasColor" + ], + "object": "ColorValue", + "labels": { + "de": "Farbe", + "en": "Colour", + "fr": "Couleur" + }, + "gui_element": "Colorpicker" + }, + { + "name": "hasCopyright", + "super": [ + "hasValue" + ], + "object": "TextValue", + "labels": { + "de": "Copyright", + "en": "Copyright", + "fr": "Droit d'auteur" + }, + "gui_element": "SimpleText", + "gui_attributes": { + "maxlength": 128, + "size": 64 + } + }, + { + "name": "isPublic", + "super": [ + "hasValue" + ], + "object": "BooleanValue", + "labels": { + "de": "Öffentlich", + "en": "Public", + "fr": "Public" + }, + "gui_element": "Checkbox" + }, + { + "name": "hasChildren", + "super": [ + "hasValue" + ], + "object": "IntValue", + "labels": { + "de": "Anzahl der Kinder", + "en": "Number of children", + "fr": "Nombre d'enfants" + }, + "gui_element": "Spinbox", + "gui_attributes": { + "max": 25.0, + "min": 0.0 + } + }, + { + "name": "hasWeight", + "super": [ + "hasValue" + ], + "object": "DecimalValue", + "labels": { + "de": "Gewicht", + "en": "Weight", + "fr": "Poids" + }, + "gui_element": "SimpleText", + "gui_attributes": { + "maxlength": 255, + "size": 80 + } + }, + { + "name": "hasFiles", + "super": [ + "hasLinkTo" + ], + "object": ":Document", + "labels": { + "de": "Modell-Dateien", + "en": "Files belonging to the model", + "fr": "Fichiers appartenants au modèle" + }, + "gui_element": "Searchbox" + }, + { + "name": "linksToRegion", + "super": [ + "hasLinkTo" + ], + "object": "Region", + "labels": { + "de": "verweist auf eine Region in einem Bild", + "en": "links to a region of an image", + "fr": "réfère à une région d'une image" + }, + "gui_element": "Searchbox" + }, + { + "name": "hasComment", + "super": [ + "hasComment" + ], + "object": "TextValue", + "labels": { + "de": "Kommentar", + "en": "Comment", + "fr": "Commentaire" + }, + "gui_element": "SimpleText" + }, + { + "name": "hasRepresentation", + "super": [ + "hasRepresentation" + ], + "object": "Representation", + "labels": { + "en": "Represented by" + }, + "gui_element": "Searchbox" + } + ], + "resources": [ + { + "name": "Text", + "labels": { + "de": "Text", + "en": "Text", + "fr": "Texte" + }, + "super": "Resource", + "cardinalities": [ + { + "propname": ":hasTextMedium", + "cardinality": "0-1", + "gui_order": 0 + }, + { + "propname": ":hasAuthor", + "cardinality": "0-1", + "gui_order": 1 + }, + { + "propname": ":hasTitle", + "cardinality": "0-1", + "gui_order": 2 + }, + { + "propname": ":hasOriginalText", + "cardinality": "0-1", + "gui_order": 3 + }, + { + "propname": ":hasTranscription", + "cardinality": "0-1", + "gui_order": 4 + }, + { + "propname": ":hasTransliteration", + "cardinality": "0-1", + "gui_order": 5 + }, + { + "propname": ":hasTranslation", + "cardinality": "0-n", + "gui_order": 6 + }, + { + "propname": ":hasFindspot", + "cardinality": "0-1", + "gui_order": 7 + }, + { + "propname": ":hasBibliographicReference", + "cardinality": "0-n", + "gui_order": 8 + }, + { + "propname": ":inInstitution", + "cardinality": "0-n", + "gui_order": 9 + }, + { + "propname": ":hasLocation", + "cardinality": "0-1", + "gui_order": 10 + }, + { + "propname": ":hasCopyright", + "cardinality": "0-1", + "gui_order": 11 + }, + { + "propname": ":hasExternalLink", + "cardinality": "0-n", + "gui_order": 12 + }, + { + "propname": ":hasRelatedArtwork", + "cardinality": "0-1", + "gui_order": 13 + } + ] + }, + { + "name": "Image2D", + "labels": { + "de": "2D-Bild", + "en": "2D image", + "fr": "image 2D" + }, + "super": "StillImageRepresentation", + "cardinalities": [ + { + "propname": ":hasTitle", + "cardinality": "1", + "gui_order": 0 + }, + { + "propname": ":hasCreator", + "cardinality": "0-1", + "gui_order": 1 + }, + { + "propname": ":hasDate", + "cardinality": "0-1", + "gui_order": 2 + }, + { + "propname": ":hasDescription", + "cardinality": "0-n", + "gui_order": 3 + }, + { + "propname": ":hasCopyright", + "cardinality": "0-1", + "gui_order": 4 + }, + { + "propname": ":inInstitution", + "cardinality": "0-n", + "gui_order": 5 + }, + { + "propname": ":hasLocation", + "cardinality": "0-1", + "gui_order": 6 + }, + { + "propname": ":hasExternalLink", + "cardinality": "0-n", + "gui_order": 7 + }, + { + "propname": ":linksToRegion", + "cardinality": "0-n", + "gui_order": 8 + } + ] + }, + { + "name": "ImagePartOfABook", + "labels": { + "de": "Bild in einem Buch", + "en": "Image in a book", + "fr": "Image dans un livre" + }, + "comments": { + "en": "Image that forms part of a book" + }, + "super": "StillImageRepresentation", + "cardinalities": [ + { + "propname": ":hasTitle", + "cardinality": "1" + }, + { + "propname": "seqnum", + "cardinality": "1" + }, + { + "propname": "isPartOf", + "cardinality": "1" + } + ] + }, + { + "name": "Image3D", + "labels": { + "de": "3D-Bild", + "en": "3D image", + "fr": "image 3D" + }, + "super": "DDDRepresentation", + "cardinalities": [ + { + "propname": ":hasTitle", + "cardinality": "1", + "gui_order": 0 + }, + { + "propname": ":hasCreator", + "cardinality": "0-1", + "gui_order": 1 + }, + { + "propname": ":hasDate", + "cardinality": "0-1", + "gui_order": 2 + }, + { + "propname": ":hasDescription", + "cardinality": "0-1", + "gui_order": 3 + }, + { + "propname": ":hasCopyright", + "cardinality": "0-1", + "gui_order": 4 + }, + { + "propname": ":hasFiles", + "cardinality": "0-1", + "gui_order": 5 + }, + { + "propname": ":inInstitution", + "cardinality": "0-n", + "gui_order": 6 + }, + { + "propname": ":hasLocation", + "cardinality": "0-1", + "gui_order": 7 + }, + { + "propname": ":hasExternalLink", + "cardinality": "0-n", + "gui_order": 8 + } + ] + }, + { + "name": "Audio", + "labels": { + "de": "Audio", + "en": "Audio", + "fr": "Audio" + }, + "super": "AudioRepresentation", + "cardinalities": [ + { + "propname": ":hasTitle", + "cardinality": "1", + "gui_order": 1 + }, + { + "propname": ":hasCreator", + "cardinality": "0-1", + "gui_order": 2 + }, + { + "propname": ":hasDate", + "cardinality": "0-1", + "gui_order": 3 + }, + { + "propname": ":hasDescription", + "cardinality": "0-1", + "gui_order": 4 + }, + { + "propname": ":hasCopyright", + "cardinality": "0-1", + "gui_order": 5 + }, + { + "propname": ":inInstitution", + "cardinality": "0-n", + "gui_order": 6 + }, + { + "propname": ":hasLocation", + "cardinality": "0-1", + "gui_order": 7 + }, + { + "propname": ":hasExternalLink", + "cardinality": "0-n", + "gui_order": 8 + } + ] + }, + { + "name": "Video", + "labels": { + "de": "Video", + "en": "Video", + "fr": "Video" + }, + "super": "MovingImageRepresentation", + "cardinalities": [ + { + "propname": ":hasTitle", + "cardinality": "1", + "gui_order": 1 + }, + { + "propname": ":hasCreator", + "cardinality": "0-1", + "gui_order": 2 + }, + { + "propname": ":hasDate", + "cardinality": "0-1", + "gui_order": 3 + }, + { + "propname": ":hasDescription", + "cardinality": "0-1", + "gui_order": 4 + }, + { + "propname": ":hasCopyright", + "cardinality": "0-1", + "gui_order": 5 + }, + { + "propname": ":inInstitution", + "cardinality": "0-n", + "gui_order": 6 + }, + { + "propname": ":hasLocation", + "cardinality": "0-1", + "gui_order": 7 + }, + { + "propname": ":hasExternalLink", + "cardinality": "0-n", + "gui_order": 8 + } + ] + }, + { + "name": "VideoSequence", + "labels": { + "de": "Sequenz einer Video-Ressource", + "en": "Sequence of a video resource" + }, + "super": "Resource", + "cardinalities": [ + { + "propname": ":hasTitle", + "cardinality": "1" + }, + { + "propname": "isSequenceOf", + "cardinality": "1" + }, + { + "propname": "hasSequenceBounds", + "cardinality": "1" + }, + { + "propname": ":hasCreator", + "cardinality": "0-1" + }, + { + "propname": ":hasDate", + "cardinality": "0-1" + }, + { + "propname": ":hasDescription", + "cardinality": "0-1" + } + ] + }, + { + "name": "AudioSequence", + "labels": { + "de": "Sequenz einer Audio-Ressource", + "en": "Sequence of an audio resource" + }, + "super": "Resource", + "cardinalities": [ + { + "propname": ":hasTitle", + "cardinality": "1" + }, + { + "propname": "isSequenceOf", + "cardinality": "1" + }, + { + "propname": "hasSequenceBounds", + "cardinality": "1" + }, + { + "propname": ":hasCreator", + "cardinality": "0-1" + }, + { + "propname": ":hasDate", + "cardinality": "0-1" + }, + { + "propname": ":hasDescription", + "cardinality": "0-1" + } + ] + }, + { + "name": "Book", + "labels": { + "de": "Buch / Manuskript", + "en": "book / manuscript", + "fr": "livre / manuscrit" + }, + "super": "Resource", + "cardinalities": [ + { + "propname": ":hasTitle", + "cardinality": "1-n", + "gui_order": 1 + }, + { + "propname": ":hasDescription", + "cardinality": "0-1", + "gui_order": 2 + }, + { + "propname": ":hasAuthor", + "cardinality": "0-n", + "gui_order": 3 + }, + { + "propname": ":hasDate", + "cardinality": "0-1", + "gui_order": 4 + }, + { + "propname": ":inInstitution", + "cardinality": "0-n", + "gui_order": 5 + }, + { + "propname": ":hasLocation", + "cardinality": "0-1", + "gui_order": 6 + }, + { + "propname": ":hasCopyright", + "cardinality": "0-1", + "gui_order": 7 + }, + { + "propname": ":hasExternalLink", + "cardinality": "0-n", + "gui_order": 8 + } + ] + }, + { + "name": "Page", + "super": "Resource", + "labels": { + "de": "Buchseite / Manuskriptseite", + "en": "Book page / Manuscript page", + "fr": "Page du livre / Page du manuscrit" + }, + "comments": { + "en": "A page is a part of a book or manuscript" + }, + "cardinalities": [ + { + "propname": ":hasTextMedium", + "cardinality": "0-1", + "gui_order": 0 + }, + { + "propname": ":hasPagenum", + "cardinality": "1", + "gui_order": 1 + }, + { + "propname": ":hasDescription", + "cardinality": "0-1", + "gui_order": 2 + }, + { + "propname": ":partOf", + "cardinality": "1", + "gui_order": 3 + }, + { + "propname": ":hasTranscription", + "cardinality": "0-1", + "gui_order": 4 + }, + { + "propname": ":hasTransliteration", + "cardinality": "0-1", + "gui_order": 5 + }, + { + "propname": ":hasTranslation", + "cardinality": "0-n", + "gui_order": 6 + }, + { + "propname": ":hasBibliographicReference", + "cardinality": "0-n", + "gui_order": 7 + } + ] + }, + { + "name": "Person", + "labels": { + "de": "Person", + "en": "Person", + "fr": "Personne" + }, + "super": "Resource", + "cardinalities": [ + { + "propname": ":hasName", + "cardinality": "1", + "gui_order": 1 + }, + { + "propname": ":hasIdentifier", + "cardinality": "0-1", + "gui_order": 2 + }, + { + "propname": ":hasChildren", + "cardinality": "0-1", + "gui_order": 3 + }, + { + "propname": ":hasExternalLink", + "cardinality": "0-n", + "gui_order": 4 + } + ] + }, + { + "name": "Institution", + "labels": { + "de": "Institution", + "en": "Institution", + "fr": "Institution" + }, + "super": "Resource", + "cardinalities": [ + { + "propname": ":hasName", + "cardinality": "1", + "gui_order": 1 + }, + { + "propname": ":hasLocation", + "cardinality": "1", + "gui_order": 2 + }, + { + "propname": ":hasIdentifier", + "cardinality": "0-1", + "gui_order": 3 + }, + { + "propname": ":hasExternalLink", + "cardinality": "0-n", + "gui_order": 4 + } + ] + }, + { + "name": "Object", + "labels": { + "de": "Objekt", + "en": "Object", + "fr": "Objet" + }, + "super": "Resource", + "cardinalities": [ + { + "propname": ":hasImage", + "cardinality": "0-n", + "gui_order": 0 + }, + { + "propname": ":hasCategory", + "cardinality": "0-n", + "gui_order": 1 + }, + { + "propname": ":hasFlatList", + "cardinality": "0-n", + "gui_order": 1 + }, + { + "propname": ":hasName", + "cardinality": "0-n", + "gui_order": 2 + }, + { + "propname": ":hasDescription", + "cardinality": "0-n", + "gui_order": 3 + }, + { + "propname": ":hasWeight", + "cardinality": "0-n", + "gui_order": 4 + }, + { + "propname": ":hasCreator", + "cardinality": "0-1", + "gui_order": 5 + }, + { + "propname": ":hasFindspot", + "cardinality": "0-n", + "gui_order": 6 + }, + { + "propname": ":hasDate", + "cardinality": "0-n", + "gui_order": 7 + }, + { + "propname": ":inInstitution", + "cardinality": "0-1", + "gui_order": 8 + }, + { + "propname": ":hasLocation", + "cardinality": "0-1", + "gui_order": 9 + }, + { + "propname": ":hasInventoryNumber", + "cardinality": "0-1", + "gui_order": 10 + }, + { + "propname": ":isPublic", + "cardinality": "0-1", + "gui_order": 11 + }, + { + "propname": ":hasColor", + "cardinality": "0-n", + "gui_order": 12 + }, + { + "propname": ":hasExternalLink", + "cardinality": "0-n", + "gui_order": 13 + }, + { + "propname": ":hasComment", + "cardinality": "0-n", + "gui_order": 14 + }, + { + "propname": ":hasTime", + "cardinality": "0-n", + "gui_order": 15 + } + ] + }, + { + "name": "Document", + "labels": { + "de": "Dokument", + "en": "Document", + "fr": "Document" + }, + "super": "DocumentRepresentation", + "cardinalities": [ + { + "propname": ":hasName", + "cardinality": "1", + "gui_order": 1 + }, + { + "propname": ":hasDescription", + "cardinality": "1", + "gui_order": 2 + }, + { + "propname": ":hasTime", + "cardinality": "0-1", + "gui_order": 3 + } + ] + }, + { + "name": "Archive", + "labels": { + "de": "Archiv", + "en": "Archive", + "fr": "Archive" + }, + "super": "ArchiveRepresentation", + "cardinalities": [ + { + "propname": ":hasName", + "cardinality": "1", + "gui_order": 1 + }, + { + "propname": ":hasDescription", + "cardinality": "1", + "gui_order": 2 + } + ] + }, + { + "name": "TextDocument", + "labels": { + "de": "Text-Dokument", + "en": "Text document", + "fr": "Document texte" + }, + "super": "TextRepresentation", + "cardinalities": [ + { + "propname": ":hasName", + "cardinality": "1", + "gui_order": 1 + }, + { + "propname": ":hasDescription", + "cardinality": "1", + "gui_order": 2 + } + ] + }, + { + "name": "ObjectWithDifferentRepresentations", + "labels": { + "de": "Objekt mit unterschiedlichen Repräsentationen", + "en": "Object with different representations" + }, + "comments": { + "en": "an object that can have representations of different types (audio, images,...)" + }, + "super": "Resource", + "cardinalities": [ + { + "propname": ":hasRepresentation", + "cardinality": "0-n", + "gui_order": 1 + } + ] + } + ] + }, + { + "name": "rosetta2", + "label": "rosetta2", + "properties": [ + { + "name": "hasLinkToOtherOnto", + "super": [ + "hasLinkTo" + ], + "object": "rosetta:Image2D", + "labels": { + "de": "Textträger", + "en": "Text medium", + "fr": "Support de texte" + }, + "gui_element": "Searchbox", + "gui_attributes": { + "numprops": 1 + } + } + ], + "resources": [ + { + "name": "TestResource", + "labels": { + "en": "TestResource" + }, + "super": "Resource", + "cardinalities": [ + { + "propname": ":hasLinkToOtherOnto", + "cardinality": "1", + "gui_order": 1 + } + ] + } + ] + } + ] + } +} diff --git a/docs/assets/templates/excel2xml_sample_script.py b/docs/assets/templates/excel2xml_sample_script.py new file mode 100644 index 000000000..25429c600 --- /dev/null +++ b/docs/assets/templates/excel2xml_sample_script.py @@ -0,0 +1,101 @@ +import pandas as pd +import warnings +from knora import excel2xml + +# general preparation +# ------------------- +path_to_json = "excel2xml_sample_onto.json" +main_df = pd.read_csv("excel2xml_sample_data.csv", dtype="str", sep=",") +# main_df = pd.read_excel("path-to-your-data-source", dtype="str") +# main_df.drop_duplicates(inplace = True) +# main_df.dropna(how = "all", inplace = True) +root = excel2xml.make_root(shortcode="0123", default_ontology="onto-name") +root = excel2xml.append_permissions(root) + +# create list mappings +# -------------------- +category_dict = excel2xml.create_json_list_mapping( + path_to_json=path_to_json, + list_name="category", + language_label="en" +) +category_dict_fallback = excel2xml.create_json_excel_list_mapping( + path_to_json=path_to_json, + list_name="category", + excel_values=main_df["Category"], + sep="," +) + +# create all resources +# -------------------- +for index, row in main_df.iterrows(): + resource = excel2xml.make_resource( + label=row["Resource name"], + restype=":MyResource", + id=excel2xml.make_xsd_id_compatible(row["Resource identifier"]) + ) + if excel2xml.check_notna(row["Image"]): + resource.append(excel2xml.make_bitstream_prop(row["Image"], permissions="prop-default")) + resource.append(excel2xml.make_text_prop(":name", row["Resource name"])) + resource.append(excel2xml.make_text_prop( + ":longtext", + excel2xml.PropertyElement(value=row["Long text"], permissions="prop-restricted", comment="long text", + encoding="xml") + )) + + # to get the correct category values, first split the cell, then look up the values in "category_dict", + # and if it's not there, look in "category_dict_fallback" + category_values = [category_dict.get(x.strip(), category_dict_fallback[x.strip()]) for x in + row["Category"].split(",")] + resource.append(excel2xml.make_list_prop("category", ":hasCategory", values=category_values)) + if excel2xml.check_notna(row["Complete?"]): + resource.append(excel2xml.make_boolean_prop(name=":isComplete", value=row["Complete?"])) + if excel2xml.check_notna(row["Color"]): + resource.append(excel2xml.make_color_prop(":colorprop", row["Color"])) + if pd.notna(row["Date discovered"]): + potential_date = excel2xml.find_date_in_string(row["Date discovered"]) + if potential_date: + resource.append(excel2xml.make_date_prop(":date", potential_date)) + else: + warnings.warn(f"Error in row {index + 2}: The column 'Date discovered' should contain a date, " + f"but no date was detected in the string '{row['Date discovered']}'") + if excel2xml.check_notna(row["Exact time"]): + resource.append(excel2xml.make_time_prop(":timeprop", row["Exact time"])) + if excel2xml.check_notna(row["Weight (kg)"]): + resource.append(excel2xml.make_decimal_prop(":weight", row["Weight (kg)"])) + if excel2xml.check_notna(row["Find location"]): + resource.append(excel2xml.make_geoname_prop(":location", row["Find location"])) + resource.append(excel2xml.make_integer_prop(":descendantsCount", row["Number of descendants"])) + if excel2xml.check_notna(row["Similar to"]): + resource.append(excel2xml.make_resptr_prop(":similarTo", row["Similar to"])) + if excel2xml.check_notna(row["See also"]): + resource.append(excel2xml.make_uri_prop(":url", row["See also"])) + + root.append(resource) + +# Annotation, Region, Link +# ------------------------ +annotation = excel2xml.make_annotation("Annotation of Resource 0", "annotation_of_res_0") +annotation.append(excel2xml.make_text_prop("hasComment", "This is a comment")) +annotation.append(excel2xml.make_resptr_prop("isAnnotationOf", "res_0")) +root.append(annotation) + +region = excel2xml.make_region("Region of Image 0", "region_of_image_0") +region.append(excel2xml.make_text_prop("hasComment", "This is a comment")) +region.append(excel2xml.make_color_prop("hasColor", "#5d1f1e")) +region.append(excel2xml.make_resptr_prop("isRegionOf", "image_0")) +region.append(excel2xml.make_geometry_prop( + "hasGeometry", + '{"type": "rectangle", "lineColor": "#ff3333", "lineWidth": 2, "points": [{"x": 0.08, "y": 0.16}, {"x": 0.73, ' + '"y": 0.72}], "original_index": 0}' +)) +root.append(region) + +link = excel2xml.make_link("Link between Resource 0 and 1", "link_res_0_res_1") +link.append(excel2xml.make_text_prop("hasComment", "This is a comment")) +link.append(excel2xml.make_resptr_prop("hasLinkTo", values=["res_0", "res_1"])) +root.append(link) + +# write file +# ---------- +excel2xml.write_xml(root, "data.xml") diff --git a/docs/dsp-tools-excel.md b/docs/dsp-tools-excel.md index 0e1b08dd8..526088de5 100644 --- a/docs/dsp-tools-excel.md +++ b/docs/dsp-tools-excel.md @@ -83,13 +83,6 @@ For further information about properties, see [here](./dsp-tools-create-ontologi -## Create a DSP-conform XML file from an Excel file - -[not yet implemented] - - - - ## Create a list from one or several Excel files With dsp-tools, a JSON list can be created from one or several Excel files. The command for this is documented @@ -170,3 +163,35 @@ the `name`. After the creation of the list, a validation against the JSON schema for lists is performed. An error message is printed out if the list is not valid. Furthermore, it is checked that no two nodes are the same. + + + + +## Create a DSP-conform XML file from an Excel/CSV file + +There are two use cases for a transformation from Excel/CSV to XML: + + - The CLI command `dsp-tools excel2xml` creates an XML file from an Excel/CSV file which is already structured + according to the DSP specifications. This is mostly used for DaSCH-interal data migration. + - The module `excel2xml` can be imported into a custom Python script that transforms any tabular data into an XML. This + use case is more frequent, because data from research projects have a variety of formats/structures. The module + `excel2xml` is documented [here](./dsp-tools-excel2xml.md). + + +### CLI command `excel2xml` + +The command line tool is used as follows: +```bash +dsp-tools excel2xml data-source.xlsx 1234 shortname +``` + +There are no flags/options for this command. + +The Excel file must be structured as in this image: +![img-excel2xml.png](assets/images/img-excel2xml.png) + +Some notes: + + - The special tags ``, ``, and `` are represented as resources of restype `Annotation`, +`LinkObj`, and `Region`. + - The columns "ark" and "iri" are only used for DaSCH-internal data migration. diff --git a/docs/dsp-tools-excel2xml.md b/docs/dsp-tools-excel2xml.md new file mode 100644 index 000000000..8f86e73db --- /dev/null +++ b/docs/dsp-tools-excel2xml.md @@ -0,0 +1,109 @@ +[![PyPI version](https://badge.fury.io/py/dsp-tools.svg)](https://badge.fury.io/py/dsp-tools) + +# `excel2xml`: Convert a data source to XML +dsp-tools assists you in converting a data source in CSV/XLS(X) format to an XML file. There are two use cases for a +transformation from Excel/CSV to XML: + + - The CLI command `dsp-tools excel2xml` creates an XML file from an Excel/CSV file which is already structured + according to the DSP specifications. This is mostly used for DaSCH-interal data migration. The CLI command is + documented [here](dsp-tools-excel.md#cli-command-excel2xml). + - The module `excel2xml` can be imported into a custom Python script that transforms any tabular data into an XML. This + use case is more frequent, because data from research projects have a variety of formats/structures. **This document + only treats the `excel2xml` module.** + + +## How to use the module excel2xml +At the end of this document, you find a sample Python script. In the following, it is explained how to use it. + + +### General preparation +Insert your ontology name, project shortcode, and the path to your data source. If necessary, activate one of the lines +that are commented out. +Then, the `root` element is created, which represents the `` tag of the XML document. As first children of +``, some standard permissions are added. At the end, please carefully check the permissions of the finished XML +file if they meet your requirements, and adapt them if necessary. +The standard permission of a resource is "res-default", and of a property "prop-default". If you don't specify it +otherwise, all resources and properties get these permissions. With excel2xml, it is not possible to create resources/ +properties that don't have permissions, because they would be invisible for all users except project admins and system +admins. Read more about permissions [here](./dsp-tools-xmlupload.md#how-to-use-the-permissions-attribute-in-resourcesproperties). + + +### Create list mappings +Let's assume that your data source has a column containing list values named after the "label" of the JSON project list, +instead of the "name" which is needed for the `dsp-tools xmlupload`. You need a way to get the names from the labels. +If your data source uses the labels correctly, this is an easy task: The method `create_json_list_mapping()` creates a +dictionary that maps the labels to the names. +If, however, your data source has spelling variants, you need the more sophisticated approach of +`create_json_excel_list_mapping()`: This method creates a dictionary that maps the list values in your data source to their +correct JSON project node name. This happens based on string similarity. Please carefully check the result if there are +no false matches! + + +### Create all resources +With the help of the [Python pandas library](https://pandas.pydata.org/), you can then iterate through the rows of your +Excel/CSV, and create resources and properties. Some examples of useful helper methods are: + + +#### Create an ID for a resource +The method `make_xsd_id_compatible(string)` makes a string compatible with the constraints of xsd:ID, so that it can be +used as ID of a resource. + + +#### Create a property +For every property, there is a helper function that explains itself when you hover over it. It also has a link to +the dsp-tools documentation of this property. So you don't need to worry how to construct a certain XML value for a +certain property. + +For `make_boolean_prop(cell)`, the following formats are supported: + + - true: True, "true", "True", "1", 1, "yes", "Yes" + - false: False, "false", "False", "0", 0, "no", "No" + + +#### Check if a cell contains a usable value +The method `check_notna(cell)` checks a value if it is usable in the context of data archiving. A value is considered +usable if it is + - a number (integer or float, but not np.nan) + - a boolean + - a string with at least one Unicode letter, underscore, or number, but not "None", "", "N/A", or "-" + - a PropertyElement whose "value" fulfills the above criteria + + +#### Calendar date parsing +The method `find_date_in_string(string)` tries to find a calendar date in a string. If successful, it +returns the DSP-formatted date string. + +Notes: + + - The date can be embedded in text. + - Only the first date found is returned. + - By default, dates are interpreted as CE (Christian era) in the Gregorian calendar. + - The years 0000-2999 are supported, in 4-digit form. + - Dates written with slashes are always interpreted in a European manner: 5/11/2021 is the 5th of November. + +Currently supported date formats: + +| Input | Output | +|-------------------|---------------------------------------| +| 0476_09_04 | GREGORIAN:CE:0476-09-04:CE:0476-09-04 | +| 0476-09-04 | GREGORIAN:CE:0476-09-04:CE:0476-09-04 | +| 30.4.2021 | GREGORIAN:CE:2021-04-30:CE:2021-04-30 | +| 5/11/2021 | GREGORIAN:CE:2021-11-05:CE:2021-11-05 | +| Jan 26, 1993 | GREGORIAN:CE:1993-01-26:CE:1993-01-26 | +| February26,2051 | GREGORIAN:CE:2051-02-26:CE:2051-02-26 | +| 28.2.-1.12.1515 | GREGORIAN:CE:1515-02-28:CE:1515-12-01 | +| 25.-26.2.0800 | GREGORIAN:CE:0800-02-25:CE:0800-02-26 | +| 1.9.2022-3.1.2024 | GREGORIAN:CE:2022-09-01:CE:2024-01-03 | +| 1848 | GREGORIAN:CE:1848:CE:1848 | +| 1849/1850 | GREGORIAN:CE:1849:CE:1850 | +| 1849/50 | GREGORIAN:CE:1849:CE:1850 | +| 1845-50 | GREGORIAN:CE:1845:CE:1850 | + + +## Complete example +Save the following files into a directory, and run the Python script. The features discussed in this document are +contained therein. + + - sample data: [excel2xml_sample_data.csv](assets/templates/excel2xml_sample_data.csv) + - sample ontology: [excel2xml_sample_onto.json](assets/templates/excel2xml_sample_onto.json) + - sample script: [excel2xml_sample_script.py](assets/templates/excel2xml_sample_script.py) diff --git a/docs/dsp-tools-usage.md b/docs/dsp-tools-usage.md index 1e64c519e..c3df150b4 100644 --- a/docs/dsp-tools-usage.md +++ b/docs/dsp-tools-usage.md @@ -194,6 +194,36 @@ More information about the usage of this command can be found +## Create an XML file from Excel/CSV +```bash +dsp-tools excel2xml data-source.xlsx project_shortcode ontology_name +``` + +Arguments: + + - data-source.xlsx: An Excel/CSV file that is structured according to [these requirements](dsp-tools-excel.md#cli-command-excel2xml) + - project_shortcode: The four-digit hexadecimal shortcode of the project + - ontology_name: the name of the ontology that the data belongs to + +If your data source is already structured according to the DSP specifications, but it is not in XML format yet, the +command `excel2xml` will transform it into XML. This is mostly used for DaSCH-interal data migration. There are no +flags/options for this command. The details of this command are documented [here](dsp-tools-excel.md#cli-command-excel2xml). + +If your data source is not yet structured according to the DSP specifications, you need a custom Python script for the +data transformation. For this, you might want to import the module `excel2xml` into your Python script, which is +described in the next paragraph. + + + +## Use the module `excel2xml` to convert a data source to XML +dsp-tools assists you in converting a data source in CSV/XLS(X) format to an XML file. Unlike the other features of +dsp-tools, this doesn't work via command line, but via helper methods that you can import into your own Python script. +Because every data source is different, there is no single algorithm to convert them to a DSP conform XML. Every user +has to deal with the specialties of his/her data source, but `excel2xml`'s helper methods can help a lot. Read more +about it [here](./dsp-tools-excel2xml.md). + + + ## Replace internal IDs with IRIs in XML file diff --git a/docs/index.md b/docs/index.md index d4aacc24d..06349b6bd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -32,4 +32,8 @@ dsp-tools helps you with the following tasks: - [`dsp-tools id2iri`](./dsp-tools-usage.md#replace-internal-ids-with-iris-in-xml-file) takes an XML file for bulk data import and replaces referenced internal IDs with IRIs. The mapping has to be provided with a JSON file. +- [`dsp-tools excel2xml`](./dsp-tools-usage.md#create-an-xml-file-from-excelcsv) transforms a data source that is + already structured according to the DSP specifications to XML. +- [The module excel2xml](./dsp-tools-usage.md#use-the-module-excel2xml-to-convert-a-data-source-to-xml) provides helper + methods that can be used in a Python script to convert data from a tabular format into XML. diff --git a/knora/dsp_tools.py b/knora/dsp_tools.py index 0c74db520..c7cce225a 100644 --- a/knora/dsp_tools.py +++ b/knora/dsp_tools.py @@ -15,6 +15,7 @@ from knora.dsplib.utils.onto_get import get_ontology from knora.dsplib.utils.onto_validate import validate_project from knora.dsplib.utils.xml_upload import xml_upload +from knora.excel2xml import excel2xml def program(user_args: list[str]) -> None: @@ -37,34 +38,31 @@ def program(user_args: list[str]) -> None: default_localhost = 'http://0.0.0.0:3333' default_user = 'root@example.com' default_pw = 'test' - dsp_tools_version = version('dsp-tools') now = datetime.datetime.now() # parse the arguments of the command line - parser = argparse.ArgumentParser( - description=f'dsp-tools (Version {dsp_tools_version}) DaSCH Service Platform data modelling tools (© {now.year} by DaSCH).') - - subparsers = parser.add_subparsers(title='Subcommands', description='Valid subcommands are', - help='sub-command help') + parser = argparse.ArgumentParser(description=f'dsp-tools (Version {dsp_tools_version}) DaSCH Service Platform data ' + f'modelling tools (© {now.year} by DaSCH).') + subparsers = parser.add_subparsers(title='Subcommands', description='Valid subcommands are', help='sub-command help') - parser_create = subparsers.add_parser('create', - help='Upload an ontology and/or list(s) from a JSON file to the DaSCH ' - 'Service Platform') + # create + parser_create = subparsers.add_parser('create', help='Upload an ontology and/or list(s) from a JSON file to the ' + 'DaSCH Service Platform') parser_create.set_defaults(action='create') parser_create.add_argument('-s', '--server', type=str, default=default_localhost, help=url_text) parser_create.add_argument('-u', '--user', default=default_user, help=username_text) parser_create.add_argument('-p', '--password', default=default_pw, help=password_text) - parser_create.add_argument('-V', '--validate-only', action='store_true', - help='Do only validation of JSON, no upload of the ' - 'ontology') + parser_create.add_argument('-V', '--validate-only', action='store_true', help='Do only validation of JSON, no ' + 'upload of the ontology') parser_create.add_argument('-l', '--lists-only', action='store_true', help='Upload only the list(s)') parser_create.add_argument('-v', '--verbose', action='store_true', help=verbose_text) parser_create.add_argument('-d', '--dump', action='store_true', help='dump test files for DSP-API requests') parser_create.add_argument('datamodelfile', help='path to data model file') - parser_get = subparsers.add_parser('get', - help='Get the ontology (data model) of a project from the DaSCH Service Platform.') + # get + parser_get = subparsers.add_parser('get', help='Get the ontology (data model) of a project from the DaSCH Service ' + 'Platform.') parser_get.set_defaults(action='get') parser_get.add_argument('-u', '--user', default=default_user, help=username_text) parser_get.add_argument('-p', '--password', default=default_pw, help=password_text) @@ -74,8 +72,8 @@ def program(user_args: list[str]) -> None: parser_get.add_argument('datamodelfile', help='Path to the file the ontology should be written to', default='onto.json') - parser_upload = subparsers.add_parser('xmlupload', - help='Upload data from an XML file to the DaSCH Service Platform.') + # xmlupload + parser_upload = subparsers.add_parser('xmlupload', help='Upload data from an XML file to the DaSCH Service Platform.') parser_upload.set_defaults(action='xmlupload') parser_upload.add_argument('-s', '--server', type=str, default=default_localhost, help=url_text) parser_upload.add_argument('-u', '--user', type=str, default=default_user, help=username_text) @@ -88,43 +86,43 @@ def program(user_args: list[str]) -> None: parser_upload.add_argument('-I', '--incremental', action='store_true', help='Incremental XML upload') parser_upload.add_argument('xmlfile', help='path to xml file containing the data', default='data.xml') - parser_excel_lists = subparsers.add_parser('excel', - help='Create a JSON list from one or multiple Excel files. The JSON ' - 'list can be integrated into a JSON ontology. If the list should ' - 'contain multiple languages, an Excel file has to be used for ' - 'each language. The filenames should contain the language as ' - 'label, p.ex. liste_de.xlsx, list_en.xlsx. The language is then ' - 'taken from the filename. Only files with extension .xlsx are ' - 'considered.') + # excel + parser_excel_lists = subparsers.add_parser( + 'excel', + help='Create a JSON list from one or multiple Excel files. The JSON list can be integrated into a JSON ' + 'ontology. If the list should contain multiple languages, an Excel file has to be used for each language. ' + 'The filenames should contain the language as label, p.ex. liste_de.xlsx, list_en.xlsx. The language is ' + 'then taken from the filename. Only files with extension .xlsx are considered.' + ) parser_excel_lists.set_defaults(action='excel') parser_excel_lists.add_argument('-l', '--listname', type=str, - help='Name of the list to be created (filename is taken if ' - 'omitted)', default=None) + help='Name of the list to be created (filename is taken if omitted)', default=None) parser_excel_lists.add_argument('excelfolder', help='Path to the folder containing the Excel file(s)', default='lists') parser_excel_lists.add_argument('outfile', help='Path to the output JSON file containing the list data', default='list.json') - parser_excel_resources = subparsers.add_parser('excel2resources', - help='Create a JSON file from an Excel file containing ' - 'resources for a DSP ontology. ') + # excel2resources + parser_excel_resources = subparsers.add_parser('excel2resources', help='Create a JSON file from an Excel file ' + 'containing resources for a DSP ontology. ') parser_excel_resources.set_defaults(action='excel2resources') parser_excel_resources.add_argument('excelfile', help='Path to the Excel file containing the resources', default='resources.xlsx') parser_excel_resources.add_argument('outfile', help='Path to the output JSON file containing the resource data', default='resources.json') - parser_excel_properties = subparsers.add_parser('excel2properties', - help='Create a JSON file from an Excel file containing ' - 'properties for a DSP ontology. ') + # excel2properties + parser_excel_properties = subparsers.add_parser('excel2properties', help='Create a JSON file from an Excel file ' + 'containing properties for a DSP ontology. ') parser_excel_properties.set_defaults(action='excel2properties') parser_excel_properties.add_argument('excelfile', help='Path to the Excel file containing the properties', default='properties.xlsx') parser_excel_properties.add_argument('outfile', help='Path to the output JSON file containing the properties data', default='properties.json') - parser_id2iri = subparsers.add_parser('id2iri', - help='Replace internal IDs in an XML with their corresponding IRIs from a provided JSON file.') + # id2iri + parser_id2iri = subparsers.add_parser('id2iri', help='Replace internal IDs in an XML with their corresponding IRIs ' + 'from a provided JSON file.') parser_id2iri.set_defaults(action='id2iri') parser_id2iri.add_argument('xmlfile', help='Path to the XML file containing the data to be replaced') parser_id2iri.add_argument('jsonfile', @@ -133,6 +131,16 @@ def program(user_args: list[str]) -> None: help='Path to the XML output file containing the replaced IDs (optional)') parser_id2iri.add_argument('-v', '--verbose', action='store_true', help=verbose_text) + # excel2xml + parser_excel2xml = subparsers.add_parser('excel2xml', help='Transform a tabular data source in CSV/XLS(X) format ' + 'to DSP-conforming XML. ') + parser_excel2xml.set_defaults(action='excel2xml') + parser_excel2xml.add_argument('datafile', help='Path to the CSV or XLS(X) file containing the data') + parser_excel2xml.add_argument('shortcode', help='Shortcode of the project that this data belongs to') + parser_excel2xml.add_argument('default_ontology', help='Name of the ontology that this data belongs to') + + + # call the requested action args = parser.parse_args(user_args) if not hasattr(args, 'action'): @@ -150,10 +158,9 @@ def program(user_args: list[str]) -> None: password=args.password, dump=args.dump) else: - if args.validate_only: - if validate_project(args.datamodelfile): - print('Data model is syntactically correct and passed validation.') - exit(0) + if args.validate_only and validate_project(args.datamodelfile): + print('Data model is syntactically correct and passed validation.') + exit(0) else: create_project(input_file=args.datamodelfile, server=args.server, @@ -193,6 +200,10 @@ def program(user_args: list[str]) -> None: json_file=args.jsonfile, out_file=args.outfile, verbose=args.verbose) + elif args.action == 'excel2xml': + excel2xml(datafile=args.datafile, + shortcode=args.shortcode, + default_ontology=args.default_ontology) def main() -> None: diff --git a/knora/dsplib/models/value.py b/knora/dsplib/models/value.py index 4d0f63f63..da77f13cb 100644 --- a/knora/dsplib/models/value.py +++ b/knora/dsplib/models/value.py @@ -1,6 +1,7 @@ import re from typing import Optional, Any, Union +import regex from pystrict import strict from .helpers import IriTest, Actions, BaseError @@ -269,7 +270,7 @@ def __init__(self, # A knora date value # m = re.match( - '(GREGORIAN:|JULIAN:)?(CE:|BCE:)?(\\d{4})?(-\\d{1,2})?(-\\d{1,2})?(:CE|:BCE)?(:\\d{4})?(-\\d{1,2})?(-\\d{1,2})?', + r"^(GREGORIAN:|JULIAN:)?(CE:|BCE:)?(\d{4})(-\d{1,2})?(-\d{1,2})?((:CE|:BCE)?(:\d{4})(-\d{1,2})?(-\d{1,2})?)?$", str(value)) if not m: raise BaseError("Invalid date format: \"{}\"!".format(str(value))) @@ -279,10 +280,10 @@ def __init__(self, self._y1 = None if dp[2] is None else int(dp[2].strip('-: ')) self._m1 = None if dp[3] is None else int(dp[3].strip('-: ')) self._d1 = None if dp[4] is None else int(dp[4].strip('-: ')) - self._e2 = 'CE' if dp[5] is None else dp[5].strip('-: ') - self._y2 = None if dp[6] is None else int(dp[6].strip('-: ')) - self._m2 = None if dp[7] is None else int(dp[7].strip('-: ')) - self._d2 = None if dp[8] is None else int(dp[8].strip('-: ')) + self._e2 = 'CE' if dp[6] is None else dp[6].strip('-: ') + self._y2 = None if dp[7] is None else int(dp[7].strip('-: ')) + self._m2 = None if dp[8] is None else int(dp[8].strip('-: ')) + self._d2 = None if dp[9] is None else int(dp[9].strip('-: ')) if self._y1 is None: raise BaseError("Invalid date format! " + str(value)) @@ -636,7 +637,9 @@ def __init__(self, iri: Optional[str] = None, ark_url: Optional[str] = None, vark_url: Optional[str] = None): - m = re.match("^(http)s?://([\\w\\.\\-~]+)?(:\\d{,6})?(/[\\w\\-~]+)*(#[\\w\\-~]*)?", str(value)) + # URI = scheme ":" ["//" host [":" port]] path ["?" query] ["#" fragment] + m = regex.match(r"(?[a-z][a-z0-9+.\-]*):(//(?[\w_.\-\[\]:~]+)(?:\d{0,6})?)(?/[\w%()_\-.~]*)*" + r"(?\?[\w_.\-=]+)*(?#[\w_/\-~:.]*)?", str(value), flags=regex.UNICODE) if m: self._value = str(value) else: @@ -688,7 +691,7 @@ def __init__(self, iri: Optional[str] = None, ark_url: Optional[str] = None, vark_url: Optional[str] = None): - m = re.match("^([+-])?(\\d{4}-[0-1]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d)(.\\d+)?(Z|[+-][0-2]\\d:[0-5]\\d)$", + m = re.match(r"^\d{4}-[0-1]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d(.\d{1,12})?(Z|[+-][0-1]\d:[0-5]\d)$", str(value)) if m: self._value = str(value) diff --git a/knora/dsplib/schemas/data.xsd b/knora/dsplib/schemas/data.xsd index a863466b6..e49b81d72 100644 --- a/knora/dsplib/schemas/data.xsd +++ b/knora/dsplib/schemas/data.xsd @@ -23,7 +23,15 @@ + value="(GREGORIAN:|JULIAN:)?(CE:|BCE:)?(\d{4})(-\d{1,2})?(-\d{1,2})?((:CE|:BCE)?(:\d{4})(-\d{1,2})?(-\d{1,2})?)?"/> + + + + + + + @@ -34,6 +42,13 @@ + + + + + + + @@ -163,7 +178,7 @@ - + @@ -183,7 +198,7 @@ - + diff --git a/knora/dsplib/utils/excel_to_json_lists.py b/knora/dsplib/utils/excel_to_json_lists.py index bb70e6e42..d82528da5 100644 --- a/knora/dsplib/utils/excel_to_json_lists.py +++ b/knora/dsplib/utils/excel_to_json_lists.py @@ -137,7 +137,7 @@ def _get_values_from_excel( f"{cell.column}, row {cell.row}:\n'{cell.value.strip()}'") # create a simplified version of the cell value and use it as name of the node - nodename = _simplify_name(cell.value.strip()) + nodename = simplify_name(cell.value.strip()) list_of_previous_node_names.append(nodename) # append a number (p.ex. node-name-2) if there are list nodes with identical names @@ -244,7 +244,7 @@ def _contains_duplicates(list_to_check: list[Any]) -> bool: return has_duplicates -def _simplify_name(value: str) -> str: +def simplify_name(value: str) -> str: """ Simplifies a given value in order to use it as node name diff --git a/knora/excel2xml.py b/knora/excel2xml.py new file mode 100644 index 000000000..0a41a70a4 --- /dev/null +++ b/knora/excel2xml.py @@ -0,0 +1,2002 @@ +import datetime +import json +import os +import re +import uuid +import warnings +import difflib +from operator import xor +import regex + +import pandas as pd +from typing import Any, Iterable, Optional, Union +from lxml import etree +from lxml.builder import E +import dataclasses + +from knora.dsplib.models.helpers import BaseError +from knora.dsplib.utils.excel_to_json_lists import simplify_name + +############################## +# global variables and classes +############################## +xml_namespace_map = { + None: "https://dasch.swiss/schema", + "xsi": "http://www.w3.org/2001/XMLSchema-instance" +} + + +@dataclasses.dataclass(frozen=True) +class PropertyElement: + """ + A PropertyElement object carries more information about a property value than the value itself. + The "value" is the value that could be passed to a method as plain string/int/float/bool. Use a PropertyElement + instead to define more precisely what attributes your tag (for example) will have. + + Args: + value: This is the content that will be written between the tags (for example) + permissions: This is the permissions that your tag (for example) will have + comment: This is the comment that your tag (for example) will have + encoding: For tags only. Can be "xml" or "utf8". + + Examples: + See the difference between the first and the second example: + + >>> make_text_prop(":testproperty", "first text") + + + first text + + + >>> make_text_prop(":testproperty", PropertyElement("first text", permissions="prop-restricted", encoding="xml")) + + + first text + + + """ + value: Union[str, int, float, bool] + permissions: str = "prop-default" + comment: Optional[str] = None + encoding: Optional[str] = None + + def __post_init__(self) -> None: + if not check_notna(self.value): + raise BaseError(f"'{self.value}' is not a valid value for a PropertyElement") + if self.encoding not in ["utf8", "xml", None]: + raise BaseError(f"'{self.encoding}' is not a valid encoding for a PropertyElement") + + +########### +# functions +########### +def make_xsd_id_compatible(string: str) -> str: + """ + Make a string compatible with the constraints of xsd:ID as defined in http://www.datypic.com/sc/xsd/t-xsd_ID.html. + An xsd:ID cannot contain special characters, and it must be unique in the document. + + This method replaces the illegal characters by "_" and appends a random number to the string to make it unique. + + The string must contain at least one word-character (regex [A-Za-z0-9_]), but must not be "None", "", "N/A", or + "-". In such cases, a BaseError is thrown. + + Args: + string: string which to make the xsd:ID from + + Returns: + an `xsd:ID` based on string + """ + + if not isinstance(string, str) or not check_notna(string): + raise BaseError(f"The string {string} cannot be made an xsd:ID") + + # if start of string is neither letter nor underscore, add an underscore + res = re.sub(r"^(?=[^A-Za-z_])", "_", string) + + # add uuid + _uuid = uuid.uuid4() + res = f"{res}_{_uuid}" + + # replace all illegal characters by underscore + res = re.sub(r"[^\d\w_\-.]", "_", res) + + return res + + +def find_date_in_string(string: str, calling_resource: str = "") -> Optional[str]: + """ + Checks if a string contains a date value (single date, or date range), and returns the first found date as + DSP-formatted string. Returns None if no date was found. + + Notes: + - Assumes Christian era (no BC dates) and Gregorian calendar. + - The years 0000-2999 are supported, in 4-digit form. + - Dates written with slashes are always interpreted in a European manner: 5/11/2021 is the 5th of November. + + Currently supported date formats: + - 0476-09-04 -> GREGORIAN:CE:0476-09-04:CE:0476-09-04 + - 0476_09_04 -> GREGORIAN:CE:0476-09-04:CE:0476-09-04 + - 30.4.2021 -> GREGORIAN:CE:2021-04-30:CE:2021-04-30 + - 5/11/2021 -> GREGORIAN:CE:2021-11-05:CE:2021-11-05 + - Jan 26, 1993 -> GREGORIAN:CE:1993-01-26:CE:1993-01-26 + - February26,2051 -> GREGORIAN:CE:2051-02-26:CE:2051-02-26 + - 28.2.-1.12.1515 --> GREGORIAN:CE:1515-02-28:CE:1515-12-01 + - 25.-26.2.0800 --> GREGORIAN:CE:0800-02-25:CE:0800-02-26 + - 1.9.2022-3.1.2024 --> GREGORIAN:CE:2022-09-01:CE:2024-01-03 + - 1848 -> GREGORIAN:CE:1848:CE:1848 + - 1849/1850 -> GREGORIAN:CE:1849:CE:1850 + - 1849/50 -> GREGORIAN:CE:1849:CE:1850 + - 1845-50 -> GREGORIAN:CE:1845:CE:1850 + + Args: + string: string to check + calling_resource: the name of the parent resource (for better error messages) + + Returns: + DSP-formatted date string, or None + + Examples: + >>> if find_date_in_string(row["Epoch"]): + >>> resource.append(make_date_prop(":hasDate", find_date_in_string(row["Epoch"])) + + See https://docs.dasch.swiss/latest/DSP-TOOLS/dsp-tools-xmlupload/#date-prop + """ + + monthes_dict = { + "January": 1, + "Jan": 1, + "February": 2, + "Feb": 2, + "March": 3, + "Mar": 3, + "April": 4, + "Apr": 4, + "May": 5, + "June": 6, + "Jun": 6, + "July": 7, + "Jul": 7, + "August": 8, + "Aug": 8, + "September": 9, + "Sept": 9, + "October": 10, + "Oct": 10, + "November": 11, + "Nov": 11, + "December": 12, + "Dec": 12, + } + + startdate: Optional[datetime.date] = None + enddate: Optional[datetime.date] = None + startyear: Optional[int] = None + endyear: Optional[int] = None + + year_regex = r"([0-2][0-9][0-9][0-9])" + month_regex = r"([0-1]?[0-9])" + day_regex = r"([0-3]?[0-9])" + sep_regex = r"[\./]" + lookbehind = r"(? bool: + """ + Check a value if it is usable in the context of data archiving. A value is considered usable if it is + - a number (integer or float, but not np.nan) + - a boolean + - a string with at least one Unicode letter, underscore, or number, but not "None", "", "N/A", or "-" + - a PropertyElement whose "value" fulfills the above criteria + + Args: + value: any object encountered when analysing data + + Returns: + True if the value is usable, False if it is N/A or otherwise unusable + """ + + if isinstance(value, PropertyElement): + value = value.value + + if any([ + isinstance(value, int), + isinstance(value, float) and pd.notna(value), # necessary because isinstance(np.nan, float) + isinstance(value, bool) + ]): + return True + elif isinstance(value, str): + return all([ + regex.search(r"\p{L}|\d|_", value, flags=re.UNICODE), + not bool(re.search(r"^(none||-|n/a)$", value, flags=re.IGNORECASE)) + ]) + else: + return False + + +def _check_and_prepare_values( + value: Optional[Union[PropertyElement, str, int, float, bool]], + values: Optional[Iterable[Union[PropertyElement, str, int, float, bool]]], + name: str, + calling_resource: str = "" +) -> list[PropertyElement]: + """ + There is a variety of possibilities how to call a make_*_prop() method. Before such a method can do its job, the + parameters need to be checked and prepared, which is done by this helper method. The parameters "value" and "values" + are passed to it as they were received. This method will then perform the following checks, and throw a BaseError in + case of failure: + - check that exactly one of them contains data, but not both. + - check that the values are usable, and not N/A + + Then, all values are transformed to PropertyElements and returned as a list. In case of a single "value", the + resulting list contains the PropertyElement of this value. + + Args: + value: "value" as received from the caller + values: "values" as received from the caller + name: name of the property (for better error messages) + calling_resource: name of the resource (for better error messages) + + Returns: + a list of PropertyElements + """ + + # reduce 'value' to None if it is not usable + if not check_notna(value): + value = None + + # reduce 'values' to None if it is not usable + if values and not any([check_notna(val) for val in values]): + values = None + + # assert that either "value" or "values" is usable, but not both at the same time + if not value and not values: + raise BaseError(f"ERROR in resource '{calling_resource}', property '{name}': 'value' and 'values' cannot both " + f"be empty") + if value and values: + raise BaseError(f"ERROR in resource '{calling_resource}', property '{name}': You cannot provide a 'value' and " + f"a 'values' at the same time!") + + # construct the resulting list + result: list[PropertyElement] = list() + + if value: + # make a PropertyElement out of it, if necessary + if isinstance(value, PropertyElement): + result.append(value) + else: + result.append(PropertyElement(value)) + elif values: + # if "values" contains unusable elements, remove them + multiple_values = [val for val in values if check_notna(val)] + # make a PropertyElement out of them, if necessary + for elem in multiple_values: + if isinstance(elem, PropertyElement): + result.append(elem) + else: + result.append(PropertyElement(elem)) + + return result + + +def make_root(shortcode: str, default_ontology: str) -> etree.Element: + """ + Start building your XML document by creating the root element . + + Args: + shortcode: The shortcode of this project as defined in the JSON project file + default ontology: one of the ontologies of the JSON project file + + Returns: + The root element . + + Examples: + >>> root = make_root(shortcode=shortcode, default_ontology=default_ontology) + >>> root = append_permissions(root) + + See https://docs.dasch.swiss/latest/DSP-TOOLS/dsp-tools-xmlupload/#the-root-element-knora + """ + + root = etree.Element( + _tag="{%s}knora" % (xml_namespace_map[None]), + attrib={ + str(etree.QName("http://www.w3.org/2001/XMLSchema-instance", "schemaLocation")): + "https://dasch.swiss/schema " + \ + "https://raw.githubusercontent.com/dasch-swiss/dsp-tools/main/knora/dsplib/schemas/data.xsd", + "shortcode": shortcode, + "default-ontology": default_ontology + }, + nsmap=xml_namespace_map + ) + return root + + +def append_permissions(root_element: etree.Element) -> etree.Element: + """ + After having created a root element, call this method to append the four permissions "res-default", + "res-restricted", "prop-default", and "prop-restricted" to it. These four permissions are a good basis to + start with, but remember that they can be adapted, and that other permissions can be defined instead of these. + + Args: + root_element: The XML root element created by make_root() + + Returns: + The root element with the four permission blocks appended + + Examples: + >>> root = make_root(shortcode=shortcode, default_ontology=default_ontology) + >>> root = append_permissions(root) + + See https://docs.dasch.swiss/latest/DSP-TOOLS/dsp-tools-xmlupload/#describing-permissions-with-permissions-elements + """ + + PERMISSIONS = E.permissions + ALLOW = E.allow + # lxml.builder.E is a more sophisticated element factory than etree.Element. + # E.tag is equivalent to E("tag") and results in + + res_default = etree.Element("{%s}permissions" % (xml_namespace_map[None]), id="res-default") + res_default.append(ALLOW("V", group="UnknownUser")) + res_default.append(ALLOW("V", group="KnownUser")) + res_default.append(ALLOW("CR", group="Creator")) + res_default.append(ALLOW("CR", group="ProjectAdmin")) + root_element.append(res_default) + + res_restricted = PERMISSIONS(id="res-restricted") + res_restricted.append(ALLOW("RV", group="UnknownUser")) + res_restricted.append(ALLOW("V", group="KnownUser")) + res_restricted.append(ALLOW("CR", group="Creator")) + res_restricted.append(ALLOW("CR", group="ProjectAdmin")) + root_element.append(res_restricted) + + prop_default = PERMISSIONS(id="prop-default") + prop_default.append(ALLOW("V", group="UnknownUser")) + prop_default.append(ALLOW("V", group="KnownUser")) + prop_default.append(ALLOW("CR", group="Creator")) + prop_default.append(ALLOW("CR", group="ProjectAdmin")) + root_element.append(prop_default) + + prop_restricted = PERMISSIONS(id="prop-restricted") + prop_restricted.append(ALLOW("RV", group="UnknownUser")) + prop_restricted.append(ALLOW("V", group="KnownUser")) + prop_restricted.append(ALLOW("CR", group="Creator")) + prop_restricted.append(ALLOW("CR", group="ProjectAdmin")) + root_element.append(prop_restricted) + + return root_element + + +def make_resource( + label: str, + restype: str, + id: str, + permissions: str = "res-default", + ark: Optional[str] = None, + iri: Optional[str] = None +) -> etree.Element: + """ + Creates an empty resource element, with the attributes as specified by the arguments + + Args: + The arguments correspond to the attributes of the element. + + Returns: + The resource element, without any children, but with the attributes + ```` + + Examples: + >>> resource = make_resource(...) + >>> resource.append(make_text_prop(...)) + >>> root.append(resource) + + See https://docs.dasch.swiss/latest/DSP-TOOLS/dsp-tools-xmlupload/#describing-resources-with-the-resource-element + """ + + kwargs = { + "label": label, + "restype": restype, + "id": id, + "permissions": permissions, + "nsmap": xml_namespace_map + } + if ark: + kwargs["ark"] = ark + if iri: + kwargs["iri"] = iri + if ark and iri: + warnings.warn(f"Both ARK and IRI were provided for resource '{label}' ({id}). The ARK will override the IRI.", + stacklevel=2) + + resource_ = etree.Element( + "{%s}resource" % (xml_namespace_map[None]), + **kwargs + ) + return resource_ + + +def make_bitstream_prop( + path: str, + permissions: str = "prop-default", + calling_resource: str = "" +) -> etree.Element: + """ + Creates a bitstream element that points to path. + + Args: + path: path to a valid file that will be uploaded + permissions: permissions string + calling_resource: the name of the parent resource (for better error messages) + + Returns: + an etree.Element that can be appended to the parent resource with resource.append(make_*_prop(...)) + + Examples: + >>> resource = make_resource(...) + >>> resource.append(make_bitstream_prop("data/images/tree.jpg")) + >>> root.append(resource) + + See https://docs.dasch.swiss/latest/DSP-TOOLS/dsp-tools-xmlupload/#bitstream + """ + + if not os.path.isfile(path): + warnings.warn(f"The following is not a valid path: {path} (resource '{calling_resource}')", + stacklevel=2) + prop_ = etree.Element("{%s}bitstream" % (xml_namespace_map[None]), permissions=permissions, + nsmap=xml_namespace_map) + prop_.text = path + return prop_ + + +def _format_bool(unformatted: Union[bool, str, int], name: str, calling_resource: str) -> str: + if isinstance(unformatted, str): + unformatted = unformatted.lower() + if unformatted in (False, "false", "0", 0, "no"): + return "false" + elif unformatted in (True, "true", "1", 1, "yes"): + return "true" + else: + raise BaseError(f"Invalid boolean format for prop '{name}' in resource '{calling_resource}': '{unformatted}'") + + +def make_boolean_prop( + name: str, + value: Union[PropertyElement, str, int, bool], + calling_resource: str = "" +) -> etree.Element: + """ + Make a from a boolean value. The value can be provided directly or inside a PropertyElement. The + following formats are supported: + - true: (True, "true", "True", "1", 1, "yes", "Yes") + - false: (False, "false", "False", "0", 0, "no", "No") + + Unless provided as PropertyElement, the permission for every value is "prop-default". + + Args: + name: the name of this property as defined in the onto + value: a str/bool/int itself or inside a PropertyElement + calling_resource: the name of the parent resource (for better error messages) + + Returns: + an etree.Element that can be appended to the parent resource with resource.append(make_*_prop(...)) + + Examples: + >>> make_boolean_prop(":testproperty", "no") + + false + + >>> make_boolean_prop(":testproperty", PropertyElement("1", permissions="prop-restricted", comment="example")) + + true + + + See https://docs.dasch.swiss/latest/DSP-TOOLS/dsp-tools-xmlupload/#boolean-prop + """ + + # validate input + if isinstance(value, PropertyElement): + value_new = dataclasses.replace(value, value=_format_bool(value.value, name, calling_resource)) + elif isinstance(value, str) or isinstance(value, bool) or isinstance(value, int): + value_new = PropertyElement(_format_bool(value, name, calling_resource)) + else: + raise BaseError(f"Invalid boolean format for prop '{name}' in resource '{calling_resource}': '{value}'") + + # make xml structure of the value + prop_ = etree.Element( + "{%s}boolean-prop" % (xml_namespace_map[None]), + name=name, + nsmap=xml_namespace_map + ) + kwargs = {"permissions": value_new.permissions} + if check_notna(value_new.comment): + kwargs["comment"] = value_new.comment + value_ = etree.Element( + "{%s}boolean" % (xml_namespace_map[None]), + **kwargs, + nsmap=xml_namespace_map + ) + value_.text = value_new.value + prop_.append(value_) + + return prop_ + + +def make_color_prop( + name: str, + value: Optional[Union[PropertyElement, str]] = None, + values: Optional[Iterable[Union[PropertyElement, str]]] = None, + calling_resource: str = "" +) -> etree.Element: + """ + Make a from one or more colors. The color(s) can be provided as string or as PropertyElement with a + string inside. If provided as string, the permission for every value is "prop-default". + + To create one ```` child, use the param ``value``, to create more than one ```` children, use + ``values``. + + Args: + name: the name of this property as defined in the onto + value: a string/PropertyElement + values: an iterable of (usually distinct) strings/PropertyElements + calling_resource: the name of the parent resource (for better error messages) + + Returns: + an etree.Element that can be appended to the parent resource with resource.append(make_*_prop(...)) + + Examples: + >>> make_color_prop(":testproperty", "#00ff66") + + #00ff66 + + >>> make_color_prop(":testproperty", PropertyElement("#00ff66", permissions="prop-restricted", comment="example")) + + #00ff66 + + >>> make_color_prop(":testproperty", values=["#00ff66", "#000000"]) + + #00ff66 + #000000 + + + See https://docs.dasch.swiss/latest/DSP-TOOLS/dsp-tools-xmlupload/#color-prop + """ + + # check the input: prepare a list with valid values + values_new = _check_and_prepare_values( + value=value, + values=values, + name=name, + calling_resource=calling_resource + ) + + # check value type + for val in values_new: + if not re.search(r"^#[0-9a-f]{6}$", str(val.value).strip(), flags=re.IGNORECASE): + raise BaseError(f"Invalid color format for prop '{name}' in resource '{calling_resource}': '{val.value}'") + + # make xml structure of the value + prop_ = etree.Element( + "{%s}color-prop" % (xml_namespace_map[None]), + name=name, + nsmap=xml_namespace_map + ) + for val in values_new: + kwargs = {"permissions": val.permissions} + if check_notna(val.comment): + kwargs["comment"] = val.comment + value_ = etree.Element( + "{%s}color" % (xml_namespace_map[None]), + **kwargs, + nsmap=xml_namespace_map + ) + value_.text = str(val.value).strip() + prop_.append(value_) + + return prop_ + + +def make_date_prop( + name: str, + value: Optional[Union[PropertyElement, str]] = None, + values: Optional[Iterable[Union[PropertyElement, str]]] = None, + calling_resource: str = "" +) -> etree.Element: + """ + Make a from one or more dates/date ranges. The date(s) can be provided as string or as PropertyElement + with a string inside. If provided as string, the permission for every value is "prop-default". + + To create one ```` child, use the param ``value``, to create more than one ```` children, use ``values``. + + Args: + name: the name of this property as defined in the onto + value: a string/PropertyElement + values: an iterable of (usually distinct) strings/PropertyElements + calling_resource: the name of the parent resource (for better error messages) + + Returns: + an etree.Element that can be appended to the parent resource with resource.append(make_*_prop(...)) + + Examples: + >>> make_date_prop(":testproperty", "GREGORIAN:CE:2014-01-31") + + GREGORIAN:CE:2014-01-31 + + >>> make_date_prop(":testproperty", PropertyElement("GREGORIAN:CE:2014-01-31", permissions="prop-restricted", comment="example")) + + + GREGORIAN:CE:2014-01-31 + + + >>> make_date_prop(":testproperty", values=["GREGORIAN:CE:1930-09-02:CE:1930-09-03", "GREGORIAN:CE:1930-09-02:CE:1930-09-03"]) + + + GREGORIAN:CE:1930-09-02:CE:1930-09-03 + + + GREGORIAN:CE:1930-09-02:CE:1930-09-03 + + + + See https://docs.dasch.swiss/latest/DSP-TOOLS/dsp-tools-xmlupload/#date-prop + """ + + # check the input: prepare a list with valid values + values_new = _check_and_prepare_values( + value=value, + values=values, + name=name, + calling_resource=calling_resource + ) + + # check value type + for val in values_new: + if not re.search(r"^(GREGORIAN:|JULIAN:)?(CE:|BCE:)?(\d{4})(-\d{1,2})?(-\d{1,2})?" + r"((:CE|:BCE)?(:\d{4})(-\d{1,2})?(-\d{1,2})?)?$", str(val.value).strip()): + raise BaseError(f"Invalid date format for prop '{name}' in resource '{calling_resource}': '{val.value}'") + + # make xml structure of the value + prop_ = etree.Element( + "{%s}date-prop" % (xml_namespace_map[None]), + name=name, + nsmap=xml_namespace_map + ) + for val in values_new: + kwargs = {"permissions": val.permissions} + if check_notna(val.comment): + kwargs["comment"] = val.comment + value_ = etree.Element( + "{%s}date" % (xml_namespace_map[None]), + **kwargs, + nsmap=xml_namespace_map + ) + value_.text = str(val.value).strip() + prop_.append(value_) + + return prop_ + + +def make_decimal_prop( + name: str, + value: Optional[Union[PropertyElement, str]] = None, + values: Optional[Iterable[Union[PropertyElement, str]]] = None, + calling_resource: str = "" +) -> etree.Element: + """ + Make a from one or more decimal numbers. The decimal(s) can be provided as string, float, or as + PropertyElement with a string/float inside. If provided as string/float, the permission for every value is + "prop-default". + + To create one ```` child, use the param ``value``, to create more than one ```` children, use + ``values``. + + Args: + name: the name of this property as defined in the onto + value: a string/float/PropertyElement + values: an iterable of distinct strings/PropertyElements + calling_resource: the name of the parent resource (for better error messages) + + Returns: + an etree.Element that can be appended to the parent resource with resource.append(make_*_prop(...)) + + Examples: + >>> make_decimal_prop(":testproperty", "3.14159") + + 3.14159 + + >>> make_decimal_prop(":testproperty", PropertyElement("3.14159", permissions="prop-restricted", comment="example")) + + 3.14159 + + >>> make_decimal_prop(":testproperty", values=["3.14159", "2.718"]) + + 3.14159 + 2.718 + + + See https://docs.dasch.swiss/latest/DSP-TOOLS/dsp-tools-xmlupload/#decimal-prop + """ + + # check the input: prepare a list with valid values + values_new = _check_and_prepare_values( + value=value, + values=values, + name=name, + calling_resource=calling_resource + ) + + # check value type + for val in values_new: + if not re.search(r"^\d+\.\d+$", str(val.value).strip()): + raise BaseError(f"Invalid decimal format for prop '{name}' in resource '{calling_resource}': '{val.value}'") + + # make xml structure of the value + prop_ = etree.Element( + "{%s}decimal-prop" % (xml_namespace_map[None]), + name=name, + nsmap=xml_namespace_map + ) + for val in values_new: + kwargs = {"permissions": val.permissions} + if check_notna(val.comment): + kwargs["comment"] = val.comment + value_ = etree.Element( + "{%s}decimal" % (xml_namespace_map[None]), + **kwargs, + nsmap=xml_namespace_map + ) + value_.text = str(val.value) + prop_.append(value_) + + return prop_ + + +def make_geometry_prop( + name: str, + value: Optional[Union[PropertyElement, str]] = None, + values: Optional[Iterable[Union[PropertyElement, str]]] = None, + calling_resource: str = "" +) -> etree.Element: + """ + Make a from one or more areas of an image. The area(s) can be provided as JSON-string or as + PropertyElement with the JSON-string inside. If provided as string, the permission for every value is "prop-default". + + To create one ```` child, use the param ``value``, to create more than one ```` children, use + ``values``. + + Args: + name: the name of this property as defined in the onto + value: a string/PropertyElement + values: an iterable of (usually distinct) strings/PropertyElements + calling_resource: the name of the parent resource (for better error messages) + + Returns: + an etree.Element that can be appended to the parent resource with resource.append(make_*_prop(...)) + + Examples: + >>> make_geometry_prop(":testproperty", json_string) + + {JSON} + + >>> make_geometry_prop(":testproperty", PropertyElement(json_string, permissions="prop-restricted", comment="example")) + + {JSON} + + >>> make_geometry_prop(":testproperty", values=[json_string1, json_string2]) + + {JSON} + {JSON} + + + See https://docs.dasch.swiss/latest/DSP-TOOLS/dsp-tools-xmlupload/#geometry-prop + """ + + # check the input: prepare a list with valid values + values_new = _check_and_prepare_values( + value=value, + values=values, + name=name, + calling_resource=calling_resource + ) + + # check value type + for val in values_new: + try: + value_as_dict = json.loads(val.value) + assert value_as_dict["type"] in ["rectangle", "circle"] + assert isinstance(value_as_dict["points"], list) + except (json.JSONDecodeError, TypeError, IndexError, KeyError, AssertionError): + raise BaseError(f"Invalid geometry format for prop '{name}' in resource '{calling_resource}': " + f"'{val.value}'") + + # make xml structure of the value + prop_ = etree.Element( + "{%s}geometry-prop" % (xml_namespace_map[None]), + name=name, + nsmap=xml_namespace_map + ) + for val in values_new: + kwargs = {"permissions": val.permissions} + if check_notna(val.comment): + kwargs["comment"] = val.comment + value_ = etree.Element( + "{%s}geometry" % (xml_namespace_map[None]), + **kwargs, + nsmap=xml_namespace_map + ) + value_.text = str(val.value) + prop_.append(value_) + return prop_ + + +def make_geoname_prop( + name: str, + value: Optional[Union[PropertyElement, str, int]] = None, + values: Optional[Iterable[Union[PropertyElement, str, int]]] = None, + calling_resource: str = "" +) -> etree.Element: + """ + Make a from one or more geonames.org IDs. The ID(s) can be provided as string, integer, or as + PropertyElement with a string/integer inside. If provided as string/integer, the permission for every value is + "prop-default". + + To create one ```` child, use the param ``value``, to create more than one ```` children, use + ``values``. + + Args: + name: the name of this property as defined in the onto + value: a string/int/PropertyElement + values: an iterable of (usually distinct) strings/ints/PropertyElements + calling_resource: the name of the parent resource (for better error messages) + + Returns: + an etree.Element that can be appended to the parent resource with resource.append(make_*_prop(...)) + + Examples: + >>> make_geoname_prop(":testproperty", "2761369") + + 2761369 + + >>> make_geoname_prop(":testproperty", PropertyElement("2761369", permissions="prop-restricted", comment="example")) + + 2761369 + + >>> make_geoname_prop(":testproperty", values=["2761369", "1010101"]) + + 2761369 + 1010101 + + + See https://docs.dasch.swiss/latest/DSP-TOOLS/dsp-tools-xmlupload/#geoname-prop + """ + + # check the input: prepare a list with valid values + values_new = _check_and_prepare_values( + value=value, + values=values, + name=name, + calling_resource=calling_resource + ) + + # check value type + for val in values_new: + if not re.search(r"^[0-9]+$", str(val.value)): + raise BaseError(f"Invalid geoname format for prop '{name}' in resource '{calling_resource}': '{val.value}'") + + prop_ = etree.Element( + "{%s}geoname-prop" % (xml_namespace_map[None]), + name=name, + nsmap=xml_namespace_map + ) + for val in values_new: + kwargs = {"permissions": val.permissions} + if check_notna(val.comment): + kwargs["comment"] = val.comment + value_ = etree.Element( + "{%s}geoname" % (xml_namespace_map[None]), + **kwargs, + nsmap=xml_namespace_map + ) + value_.text = str(val.value) + prop_.append(value_) + + return prop_ + + +def make_integer_prop( + name: str, + value: Optional[Union[PropertyElement, str, int]] = None, + values: Optional[Iterable[Union[PropertyElement, str, int]]] = None, + calling_resource: str = "" +) -> etree.Element: + """ + Make a from one or more integers. The integers can be provided as string, integer, or as + PropertyElement with a string/integer inside. If provided as string/integer, the permission for every value is + "prop-default". + + To create one ```` child, use the param ``value``, to create more than one ```` children, use + ``values``. + + Args: + name: the name of this property as defined in the onto + value: a string/int/PropertyElement + values: an iterable of (usually distinct) strings/ints/PropertyElements + calling_resource: the name of the parent resource (for better error messages) + + Returns: + an etree.Element that can be appended to the parent resource with resource.append(make_*_prop(...)) + + Examples: + >>> make_integer_prop(":testproperty", "2761369") + + 2761369 + + >>> make_integer_prop(":testproperty", PropertyElement("2761369", permissions="prop-restricted", comment="example")) + + 2761369 + + >>> make_integer_prop(":testproperty", values=["2761369", "1010101"]) + + 2761369 + 1010101 + + + See https://docs.dasch.swiss/latest/DSP-TOOLS/dsp-tools-xmlupload/#integer-prop + """ + + # check the input: prepare a list with valid values + values_new = _check_and_prepare_values( + value=value, + values=values, + name=name, + calling_resource=calling_resource + ) + + # check value type + for val in values_new: + if not re.search(r"^\d+$", str(val.value).strip()): + raise BaseError(f"Invalid integer format for prop '{name}' in resource '{calling_resource}': '{val.value}'") + + prop_ = etree.Element( + "{%s}integer-prop" % (xml_namespace_map[None]), + name=name, + nsmap=xml_namespace_map + ) + for val in values_new: + kwargs = {"permissions": val.permissions} + if check_notna(val.comment): + kwargs["comment"] = val.comment + value_ = etree.Element( + "{%s}integer" % (xml_namespace_map[None]), + **kwargs, + nsmap=xml_namespace_map + ) + value_.text = str(val.value) + prop_.append(value_) + + return prop_ + + +def make_interval_prop( + name: str, + value: Optional[Union[PropertyElement, str]] = None, + values: Optional[Iterable[Union[PropertyElement, str]]] = None, + calling_resource: str = "" +) -> etree.Element: + """ + Make a from one or more intervals. The interval(s) can be provided as string or as PropertyElement + with a string inside. If provided as string, the permission for every value is "prop-default". + + To create one ```` child, use the param ``value``, to create more than one ```` children, use + ``values``. + + Args: + name: the name of this property as defined in the onto + value: a string/PropertyElement + values: an iterable of (usually distinct) strings/PropertyElements + calling_resource: the name of the parent resource (for better error messages) + + Returns: + an etree.Element that can be appended to the parent resource with resource.append(make_*_prop(...)) + + Examples: + >>> make_interval_prop(":testproperty", "61:3600") + + 61:3600 + + >>> make_interval_prop(":testproperty", PropertyElement("61:3600", permissions="prop-restricted", comment="example")) + + 61:3600 + + >>> make_interval_prop(":testproperty", values=["61:3600", "60.5:120.5"]) + + 61:3600 + 60.5:120.5 + + + See https://docs.dasch.swiss/latest/DSP-TOOLS/dsp-tools-xmlupload/#interval-prop + """ + + # check the input: prepare a list with valid values + values_new = _check_and_prepare_values( + value=value, + values=values, + name=name, + calling_resource=calling_resource + ) + + # check value type + for val in values_new: + if not re.match(r"([+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)):([+-]?([0-9]+([.][0-9]*)?|[.][0-9]+))", str(val.value)): + raise BaseError(f"Invalid integer format for prop '{name}' in resource '{calling_resource}': '{val.value}'") + + + prop_ = etree.Element( + "{%s}interval-prop" % (xml_namespace_map[None]), + name=name, + nsmap=xml_namespace_map + ) + for val in values_new: + kwargs = {"permissions": val.permissions} + if check_notna(val.comment): + kwargs["comment"] = val.comment + value_ = etree.Element( + "{%s}interval" % (xml_namespace_map[None]), + **kwargs, + nsmap=xml_namespace_map + ) + value_.text = val.value + prop_.append(value_) + + return prop_ + + +def make_list_prop( + list_name: str, + name: str, + value: Optional[Union[PropertyElement, str]] = None, + values: Optional[Iterable[Union[PropertyElement, str]]] = None, + calling_resource: str = "" +) -> etree.Element: + """ + Make a from one or more list items. The list item(s) can be provided as string or as PropertyElement + with a string inside. If provided as string, the permission for every value is "prop-default". + + To create one ```` child, use the param ``value``, to create more than one ```` children, use ``values``. + + Args: + list_name: the name of the list as defined in the onto + name: the name of this property as defined in the onto + value: a string/PropertyElement + values: an iterable of (usually distinct) strings/PropertyElements + calling_resource: the name of the parent resource (for better error messages) + + Returns: + an etree.Element that can be appended to the parent resource with resource.append(make_*_prop(...)) + + Examples: + >>> make_list_prop("mylist", ":testproperty", "first_node") + + first_node + + >>> make_list_prop("mylist", ":testproperty", PropertyElement("first_node", permissions="prop-restricted", comment="example")) + + first_node + + >>> make_list_prop("mylist", ":testproperty", values=["first_node", "second_node"]) + + first_node + second_node + + + See https://docs.dasch.swiss/latest/DSP-TOOLS/dsp-tools-xmlupload/#list-prop + """ + + # check the input: prepare a list with valid values + values_new = _check_and_prepare_values( + value=value, + values=values, + name=name, + calling_resource=calling_resource + ) + + # check value type + for val in values_new: + if not isinstance(val.value, str) or not check_notna(val.value): + raise BaseError(f"Invalid list format for prop '{name}' in resource '{calling_resource}': '{val.value}'") + + # make xml structure of the valid values + prop_ = etree.Element( + "{%s}list-prop" % (xml_namespace_map[None]), + list=list_name, + name=name, + nsmap=xml_namespace_map + ) + for val in values_new: + kwargs = {"permissions": val.permissions} + if check_notna(val.comment): + kwargs["comment"] = val.comment + value_ = etree.Element( + "{%s}list" % (xml_namespace_map[None]), + **kwargs, + nsmap=xml_namespace_map + ) + value_.text = val.value + prop_.append(value_) + + return prop_ + + +def make_resptr_prop( + name: str, + value: Optional[Union[PropertyElement, str]] = None, + values: Optional[Iterable[Union[PropertyElement, str]]] = None, + calling_resource: str = "" +) -> etree.Element: + """ + Make a from one or more links to other resources. The links(s) can be provided as string or as + PropertyElement with a string inside. If provided as string, the permission for every value is "prop-default". + + To create one ```` child, use the param ``value``, to create more than one ```` children, use + ``values``. + + Args: + name: the name of this property as defined in the onto + value: a string/PropertyElement + values: an iterable of (usually distinct) strings/PropertyElements + calling_resource: the name of the parent resource (for better error messages) + + Returns: + an etree.Element that can be appended to the parent resource with resource.append(make_*_prop(...)) + + Examples: + >>> make_resptr_prop(":testproperty", "resource_1") + + resource_1 + + >>> make_resptr_prop(":testproperty", PropertyElement("resource_1", permissions="prop-restricted", comment="example")) + + resource_1 + + >>> make_resptr_prop(":testproperty", values=["resource_1", "resource_2"]) + + resource_1 + resource_2 + + + See https://docs.dasch.swiss/latest/DSP-TOOLS/dsp-tools-xmlupload/#resptr-prop + """ + + # check the input: prepare a list with valid values + values_new = _check_and_prepare_values( + value=value, + values=values, + name=name, + calling_resource=calling_resource + ) + + # check value type + for val in values_new: + if not isinstance(val.value, str) or not check_notna(val.value): + raise BaseError(f"Invalid resptr format for prop '{name}' in resource '{calling_resource}': '{val.value}'") + + # make xml structure of the valid values + prop_ = etree.Element( + "{%s}resptr-prop" % (xml_namespace_map[None]), + name=name, + nsmap=xml_namespace_map + ) + for val in values_new: + kwargs = {"permissions": val.permissions} + if check_notna(val.comment): + kwargs["comment"] = val.comment + value_ = etree.Element( + "{%s}resptr" % (xml_namespace_map[None]), + **kwargs, + nsmap=xml_namespace_map + ) + value_.text = val.value + prop_.append(value_) + + return prop_ + + +def make_text_prop( + name: str, + value: Optional[Union[PropertyElement, str]] = None, + values: Optional[Iterable[Union[PropertyElement, str]]] = None, + calling_resource: str = "" +) -> etree.Element: + """ + Make a from one or more texts. The text(s) can be provided as string or as PropertyElement with a string + inside. The default encoding is utf8. The default permission for every value is "prop-default". + + To create one ```` child, use the param ``value``, to create more than one ```` + children, use ``values``. + + Args: + name: the name of this property as defined in the onto + value: a string/PropertyElement + values: an iterable of (usually distinct) strings/PropertyElements + calling_resource: the name of the parent resource (for better error messages) + + Returns: + an etree.Element that can be appended to the parent resource with resource.append(make_*_prop(...)) + + Examples: + >>> make_text_prop(":testproperty", "first text") + + first text + + >>> make_text_prop(":testproperty", PropertyElement("first text", permissions="prop-restricted", encoding="xml")) + + first text + + >>> make_text_prop(":testproperty", values=["first text", "second text"]) + + first text + second text + + + See https://docs.dasch.swiss/latest/DSP-TOOLS/dsp-tools-xmlupload/#text-prop + """ + + # check the input: prepare a list with valid values + values_new = _check_and_prepare_values( + value=value, + values=values, + name=name, + calling_resource=calling_resource + ) + + # check value type + for val in values_new: + if not isinstance(val.value, str) or not check_notna(val.value): + raise BaseError(f"Invalid text format for prop '{name}' in resource '{calling_resource}': '{val.value}'") + + # make xml structure of the valid values + prop_ = etree.Element( + "{%s}text-prop" % (xml_namespace_map[None]), + name=name, + nsmap=xml_namespace_map + ) + for val in values_new: + kwargs = {"permissions": val.permissions} + if check_notna(val.comment): + kwargs["comment"] = val.comment + if check_notna(val.encoding): + kwargs["encoding"] = val.encoding + else: + kwargs["encoding"] = "utf8" + value_ = etree.Element( + "{%s}text" % (xml_namespace_map[None]), + **kwargs, + nsmap=xml_namespace_map + ) + value_.text = val.value + prop_.append(value_) + + return prop_ + + +def make_time_prop( + name: str, + value: Optional[Union[PropertyElement, str]] = None, + values: Optional[Iterable[Union[PropertyElement, str]]] = None, + calling_resource: str = "" +) -> etree.Element: + """ + Make a from one or more datetime values of the form "2009-10-10T12:00:00-05:00". The time(s) can be + provided as string or as PropertyElement with a string inside. If provided as string, the permission for every + value is "prop-default". + + To create one `` JULIAN:BCE:0700:BCE:0600 + CE:1849:CE:1850 + GREGORIAN:1848-01:1849-02 + 2022 + GREGORIAN:CE:0476-09-04:CE:0476-09-04 + GREGORIAN:CE:2014-01-31 4711 @@ -106,7 +111,7 @@ #00ff00 - b2 + first subnode test_thing_0 @@ -181,11 +186,32 @@ false - https://dasch.swiss + https://en.wiktionary.org/wiki/Ῥόδος + https://www.test-case.ch/ + https://reg-exr.com:3000 + https://reg-exr.com:3000/path/to/file_(%C3%89).htm + https://reg-exr.com:3000/path/to/file#fragment + https://reg-exr.com:3000/path/to/file?query=test + https://reg-exr.com:3000/path/to/file?query=test#fragment + https://reg-exr.com/path/to/file?query=test#fragment + http://www.168.1.1.0/path + http://www.168.1.1.0:4200/path + http://[2001:0db8:0000:0000:0000:8a2e:0370:7334]:4200/path + https://en.wikipedia.org/wiki/Haiku#/media/File:Basho_Horohoroto.jpg + http://datypic.com/prod.html#shirt JULIAN:BCE:0700:BCE:0600 + + + + + + + + + 4711 @@ -202,7 +228,7 @@ test_thing_0 - b2 + second subnode @@ -654,7 +680,7 @@ video_thing_1 - 2.0:10.0 + +.1:+.9 @@ -667,7 +693,7 @@ audio_thing_1 - 8.0:12.0 + -.1:5 @@ -680,7 +706,7 @@ video_thing_1 - 8.0:12.0 + -10.0:-5.1 diff --git a/testdata/test-project-systematic.json b/testdata/test-project-systematic.json index bf6f6155c..fc5bba24d 100644 --- a/testdata/test-project-systematic.json +++ b/testdata/test-project-systematic.json @@ -32,36 +32,36 @@ }, "nodes": [ { - "name": "a", + "name": "first node of testlist", "labels": { - "en": "a_label with a\"post'rophes", + "en": "First node of the Test-List", "rm": "Rumantsch" } }, { - "name": "b", + "name": "second node of testlist", "labels": { - "en": "b_label with no comment" + "en": "Second node of the Test-List" }, "nodes": [ { - "name": "b1", + "name": "first subnode", "labels": { - "en": "b1_label" + "en": "First Sub-Node" } }, { - "name": "b2", + "name": "second subnode", "labels": { - "en": "b2_label" + "en": "Second Sub-Node" } } ] }, { - "name": "c", + "name": "third node of testlist", "labels": { - "en": "c_label" + "en": "Third node of the Test-List" } } ] @@ -93,7 +93,11 @@ { "name": "notUsedNode_1", "labels": { - "en": "nodeLabel_1" + "en": "nodeLabel_1\"'" + }, + "comments": { + "en": "Nodes can have comments, too!", + "rm": "Even in Rumantsch!" } } ] @@ -301,6 +305,17 @@ }, "gui_element": "Date" }, + { + "name": "hasTime", + "super": [ + "hasValue" + ], + "object": "TimeValue", + "labels": { + "en": "Time" + }, + "gui_element": "TimeStamp" + }, { "name": "hasInteger", "super": [ @@ -487,77 +502,66 @@ "cardinalities": [ { "propname": ":hasText", - "gui_order": 1, "cardinality": "1-n" }, { "propname": ":hasRichtext", - "gui_order": 2, "cardinality": "0-n" }, { "propname": ":hasUri", - "gui_order": 3, "cardinality": "0-n" }, { "propname": ":hasBoolean", - "gui_order": 4, "cardinality": "1" }, { "propname": ":hasDate", - "gui_order": 5, + "cardinality": "0-n" + }, + { + "propname": ":hasTime", "cardinality": "0-n" }, { "propname": ":hasInteger", - "gui_order": 6, "cardinality": "0-n" }, { "propname": ":hasDecimal", - "gui_order": 7, "cardinality": "0-n" }, { "propname": ":hasGeoname", - "gui_order": 9, "cardinality": "0-n" }, { "propname": ":hasColor", - "gui_order": 11, "cardinality": "0-n" }, { "propname": ":hasListItem", - "gui_order": 12, "cardinality": "0-n" }, { "propname": ":hasTestThing2", - "gui_order": 13, "cardinality": "0-n" }, { "propname": ":hasResource", - "gui_order": 14, "cardinality": "0-n" }, { "propname": ":hasRegion", - "gui_order": 15, "cardinality": "0-n" }, { "propname": ":hasRepresentation", - "gui_order": 16, "cardinality": "0-n" }, { "propname": ":hasMovingImageRepresentation", - "gui_order": 17, "cardinality": "0-n" } ]