From f17845396f109afc3442a29d24693b5e3a49f5eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Hozman?= <tomas.hozman@pm.me> Date: Fri, 24 Jun 2022 16:01:54 +0200 Subject: [PATCH] working --- Pipfile | 15 + Pipfile.lock | 337 ++++++++++++ config.example.json | 188 ++++++- measurer/__init__.py | 2 +- measurer/database/models.py | 3 + measurer/static/simple.css | 516 ------------------ measurer/templates/base.html | 11 - measurer/templates/racer_add.html | 26 - measurer/templates/racer_edit.html | 26 - measurer/templates/racer_list.html | 46 -- measurer/templates/racer_view.html | 23 - measurer/types/__init__.py | 21 + .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 1715 bytes measurer/validators/__init__.py | 338 ++++++++++++ measurer/views/measurer.py | 63 +++ measurer/views/racer.py | 0 16 files changed, 965 insertions(+), 650 deletions(-) create mode 100644 Pipfile create mode 100644 Pipfile.lock delete mode 100644 measurer/static/simple.css delete mode 100644 measurer/templates/base.html delete mode 100644 measurer/templates/racer_add.html delete mode 100644 measurer/templates/racer_edit.html delete mode 100644 measurer/templates/racer_list.html delete mode 100644 measurer/templates/racer_view.html create mode 100644 measurer/types/__init__.py create mode 100644 measurer/utils/__pycache__/__init__.cpython-310.pyc create mode 100644 measurer/validators/__init__.py create mode 100644 measurer/views/measurer.py delete mode 100644 measurer/views/racer.py diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..b25b765 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +flask = "*" +sqlalchemy = "*" +cerberus = "*" +argon2-cffi = "*" + +[dev-packages] + +[requires] +python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..9486448 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,337 @@ +{ + "_meta": { + "hash": { + "sha256": "23ba3a07b72940777356e477ceb4af1aa92774e7516dcec9038e675bdb1af013" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.9" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "argon2-cffi": { + "hashes": [ + "sha256:8c976986f2c5c0e5000919e6de187906cfd81fb1c72bf9d88c01177e77da7f80", + "sha256:d384164d944190a7dd7ef22c6aa3ff197da12962bd04b17f64d4e93d934dba5b" + ], + "index": "pypi", + "version": "==21.3.0" + }, + "argon2-cffi-bindings": { + "hashes": [ + "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670", + "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f", + "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583", + "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194", + "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c", + "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a", + "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082", + "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5", + "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f", + "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7", + "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d", + "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f", + "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae", + "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3", + "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86", + "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367", + "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d", + "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93", + "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb", + "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e", + "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351" + ], + "markers": "python_version >= '3.6'", + "version": "==21.2.0" + }, + "cerberus": { + "hashes": [ + "sha256:d1b21b3954b2498d9a79edf16b3170a3ac1021df88d197dc2ce5928ba519237c" + ], + "index": "pypi", + "version": "==1.3.4" + }, + "cffi": { + "hashes": [ + "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3", + "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2", + "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636", + "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20", + "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728", + "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27", + "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66", + "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443", + "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0", + "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7", + "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39", + "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605", + "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a", + "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37", + "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029", + "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139", + "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc", + "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df", + "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14", + "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880", + "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2", + "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a", + "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e", + "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474", + "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024", + "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8", + "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0", + "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e", + "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a", + "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e", + "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032", + "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6", + "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e", + "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b", + "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e", + "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954", + "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962", + "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c", + "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4", + "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55", + "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962", + "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023", + "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c", + "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6", + "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8", + "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382", + "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7", + "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc", + "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997", + "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796" + ], + "version": "==1.15.0" + }, + "click": { + "hashes": [ + "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", + "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.3" + }, + "flask": { + "hashes": [ + "sha256:315ded2ddf8a6281567edb27393010fe3406188bafbfe65a3339d5787d89e477", + "sha256:fad5b446feb0d6db6aec0c3184d16a8c1f6c3e464b511649c8918a9be100b4fe" + ], + "index": "pypi", + "version": "==2.1.2" + }, + "greenlet": { + "hashes": [ + "sha256:0051c6f1f27cb756ffc0ffbac7d2cd48cb0362ac1736871399a739b2885134d3", + "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711", + "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd", + "sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073", + "sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708", + "sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67", + "sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23", + "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1", + "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08", + "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd", + "sha256:2bde6792f313f4e918caabc46532aa64aa27a0db05d75b20edfc5c6f46479de2", + "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa", + "sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8", + "sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40", + "sha256:572e1787d1460da79590bf44304abbc0a2da944ea64ec549188fa84d89bba7ab", + "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6", + "sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc", + "sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b", + "sha256:7418b6bfc7fe3331541b84bb2141c9baf1ec7132a7ecd9f375912eca810e714e", + "sha256:7cbd7574ce8e138bda9df4efc6bf2ab8572c9aff640d8ecfece1b006b68da963", + "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3", + "sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d", + "sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d", + "sha256:8c5d5b35f789a030ebb95bff352f1d27a93d81069f2adb3182d99882e095cefe", + "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28", + "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3", + "sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e", + "sha256:93f81b134a165cc17123626ab8da2e30c0455441d4ab5576eed73a64c025b25c", + "sha256:95e69877983ea39b7303570fa6760f81a3eec23d0e3ab2021b7144b94d06202d", + "sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0", + "sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497", + "sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee", + "sha256:aa5b467f15e78b82257319aebc78dd2915e4c1436c3c0d1ad6f53e47ba6e2713", + "sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58", + "sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a", + "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06", + "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88", + "sha256:b336501a05e13b616ef81ce329c0e09ac5ed8c732d9ba7e3e983fcc1a9e86965", + "sha256:b8c008de9d0daba7b6666aa5bbfdc23dcd78cafc33997c9b7741ff6353bafb7f", + "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4", + "sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5", + "sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c", + "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a", + "sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1", + "sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43", + "sha256:eb6ea6da4c787111adf40f697b4e58732ee0942b5d3bd8f435277643329ba627", + "sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b", + "sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168", + "sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d", + "sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5", + "sha256:f3acda1924472472ddd60c29e5b9db0cec629fbe3c5c5accb74d6d6d14773478", + "sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf", + "sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce", + "sha256:fa877ca7f6b48054f847b61d6fa7bed5cebb663ebc55e018fda12db09dcc664c", + "sha256:fdcec0b8399108577ec290f55551d926d9a1fa6cad45882093a7a07ac5ec147b" + ], + "markers": "python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))", + "version": "==1.1.2" + }, + "importlib-metadata": { + "hashes": [ + "sha256:5d26852efe48c0a32b0509ffbc583fda1a2266545a78d104a6f4aff3db17d700", + "sha256:c58c8eb8a762858f49e18436ff552e83914778e50e9d2f1660535ffb364552ec" + ], + "markers": "python_version < '3.10'", + "version": "==4.11.4" + }, + "itsdangerous": { + "hashes": [ + "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44", + "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a" + ], + "markers": "python_version >= '3.7'", + "version": "==2.1.2" + }, + "jinja2": { + "hashes": [ + "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", + "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.2" + }, + "markupsafe": { + "hashes": [ + "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003", + "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88", + "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5", + "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7", + "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a", + "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603", + "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1", + "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135", + "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247", + "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6", + "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601", + "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77", + "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02", + "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e", + "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63", + "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f", + "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980", + "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b", + "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812", + "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff", + "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96", + "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1", + "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925", + "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a", + "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6", + "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e", + "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f", + "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4", + "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f", + "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3", + "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c", + "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a", + "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417", + "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a", + "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a", + "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37", + "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452", + "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933", + "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a", + "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7" + ], + "markers": "python_version >= '3.7'", + "version": "==2.1.1" + }, + "pycparser": { + "hashes": [ + "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", + "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + ], + "version": "==2.21" + }, + "setuptools": { + "hashes": [ + "sha256:990a4f7861b31532871ab72331e755b5f14efbe52d336ea7f6118144dd478741", + "sha256:c1848f654aea2e3526d17fc3ce6aeaa5e7e24e66e645b5be2171f3f6b4e5a178" + ], + "markers": "python_version >= '3.7'", + "version": "==62.6.0" + }, + "sqlalchemy": { + "hashes": [ + "sha256:07865d93e4ca77b59a5ce0f36fbae8161f7dfe57ba17934a3e442cf95dcb3c49", + "sha256:1ac6b091b322ec54a30c751dfcb736987e317f5c53a5cf3beb62e11a18210319", + "sha256:380e09881cdf3c87e90b8995425f7ea618e6bbd33c6b7c9234af21c4b6b3c143", + "sha256:3abe087b641788abbbe94abbf9f15f50bb985f72c0669ef35d1941d2912a276d", + "sha256:42810e560b57e981ed0a947b65a4936b398b4fca97e5b56e10a9c5a151568de2", + "sha256:42a60988aad143a4b2745711548833f57340d7f35586160140361314a509e6f7", + "sha256:470fd9d820fbd25c2a2a2929327c44aaff9d5871a20e0cadd32d293540817517", + "sha256:492f25432f0a998bcaa35e907f9d33f436d208326bb1e6c0f8485e8117502a3d", + "sha256:55c09559e45d3f067435620195238f983d4a23f796650f959f19964ba9104c6f", + "sha256:57ea67a9206eab2abe130e4fdae0662f10cca3dc72ba27553f70a7d613588571", + "sha256:63f8e68356b53072a653e8f61c5f1c19721469af4dbfdb3e3356073e9918f1fe", + "sha256:6edadd6a0a722c22558e1d1f5360d3e85fa938bc69d9049d29968a643de6dd34", + "sha256:737f4feee88d78230fa38027ad5645cb327fe9aac0dd0bde3f8fa7026ed81910", + "sha256:77831317da71adec7b785ebf9e6467b59ba1e186de1ba13c94b4e4951387ba64", + "sha256:82701a4cbb14affc6c1ae62dcebdaff65611b7c7f96f9d0e92a34a8be112a8fa", + "sha256:93ae1d2ef42fbf0f0b3d44b35225bda123310df4b33c9bf662e7b50a68c48a98", + "sha256:97ba370e31b70be94f2f1e85494a5c90f8cf50381ddc02ab95a33a4a86371e02", + "sha256:a57edcbbb45e8307153c5d4635407df71529ed263666064c0524b0c412778306", + "sha256:ad2447f17425e6889f0fb2b229844799aabafc90ff780123067fc5846a30992c", + "sha256:b33388891faf67d0c4a7bb65657dd1a068168eda4b793cb929c4c3894adfdcf2", + "sha256:b8cd779ef29718f3d2c558042ccc45c03006c599dd722fb760faca641a2f32ac", + "sha256:bdea12b997b174903292cf19f40d36cad46b44b645725b9485164684d1849bfd", + "sha256:bf05b312bf0165f92fa0eb09e7661c26f2f06c7a89694ecb79fa15a933deb768", + "sha256:c715347cac3b1c563941162fbbf751d3a5e0c356a33cb20925699f4910504a8f", + "sha256:cd1aba14bbb1ecfe8b5cc52dc840a7e071cfcce6bff545037cf56714c48dfc92", + "sha256:cf1afb1deec19de7ba282062de8a8c4f931ef120faa8b3dc6fca826bbc2f6a9d", + "sha256:cfdb1b3763aa4bddccd7b627b9466fce94952dc150a49309eb56e5f50dd00806", + "sha256:d3c4191e0348428b127c4c2e25ec9c1e8e895e3c6d9a7f083fca28dce23257ee", + "sha256:d8193b4a340d868f2daeeb856dfae9d9d4b011f249128380a83ee7342a887bdb", + "sha256:da424c8b285da91733fed2dd40fed7db076818a62859244d311b80fc8ba4d75e", + "sha256:e44e5f4d84861f4a2a00da8e55712db0dd2ec3d680544fb5d3ac84d3682d7d4c", + "sha256:eba2c5f717fa6d7be040bbc1e4334f1827d31e672cfd53ddbd995935d43e517e", + "sha256:f036bdc951b0d64c64ae83e7ff83a1848eea74f1c6e42461347caab2ed7282b9", + "sha256:f04789d723fbd6214a63006b4711d7afca37630473edb6ab972c5df2b43b7a56", + "sha256:fa64578158cb374e4dd6da2377f1ceabf9973313d171e67fc01a353aa8967858" + ], + "index": "pypi", + "version": "==1.4.38" + }, + "werkzeug": { + "hashes": [ + "sha256:1ce08e8093ed67d638d63879fd1ba3735817f7a80de3674d293f5984f25fb6e6", + "sha256:72a4b735692dd3135217911cbeaa1be5fa3f62bffb8745c5215420a03dc55255" + ], + "markers": "python_version >= '3.7'", + "version": "==2.1.2" + }, + "zipp": { + "hashes": [ + "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad", + "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099" + ], + "markers": "python_version >= '3.7'", + "version": "==3.8.0" + } + }, + "develop": {} +} diff --git a/config.example.json b/config.example.json index 41d8d6e..ee7037b 100644 --- a/config.example.json +++ b/config.example.json @@ -5,6 +5,192 @@ "IDENTIFIER_HASH_MEMORY_COST": 65536, "IDENTIFIER_HASH_PARALLELISM": 4, "POINTS": [ - + "bydleni:nerozprodame-byty", + "bydleni:transparentni-pravidla-s-bytovym-fondem", + "bydleni:pece-o-bytove-fondy", + "bydleni:spoluprace-s-ministerstvem-pro-mistni-rozvoj", + "bydleni:komunitni-bydleni", + "bydleni:dostupne-socialni-bydleni", + "bydleni:mapovani-potreb-lidi-v-obci", + "bydleni:vztah-k-developerum", + "bydleni:ferove-mistni-poplatky", + "bydleni:mmr-rekonstrukce-socialniho-bydleni", + "otevrena-radnice:jasne-informace-z-radnice", + "otevrena-radnice:transparentni-rozhodovani-ve-verejnem-zajmu", + "otevrena-radnice:pruhledne-hospodareni-s-majetkem", + "otevrena-radnice:hospodarny-transparentni-rozpocet", + "otevrena-radnice:verejna-zadavaci-dokumentace", + "otevrena-radnice:prime-zakazky-vyjimka", + "otevrena-radnice:zverejnovani-zakazek-mimo-prazdniny", + "otevrena-radnice:zadavaci-rizeni-u-opakovanych-zakazek", + "otevrena-radnice:dbat-na-projektove-dokumentace", + "otevrena-radnice:svobodny-software-a-formaty", + "otevrena-radnice:granty-a-dotace-pro-mesto", + "otevrena-radnice:odborni-zastupci-mesta-v-organech", + "otevrena-radnice:pravidla-pro-obce-i-jejich-organizace", + "otevrena-radnice:pravidla-pro-nominace-a-odmenovani-v-spolecnostech", + "otevrena-radnice:uzemni-plan", + "otevrena-radnice:mesta-bez-barier", + "otevrena-radnice:kvalitni-verejny-prostor", + "otevrena-radnice:ucast-na-rozhodovani", + "otevrena-radnice:radnicni-media", + "otevrena-radnice:nekale-jednani-a-stret-zajmu", + "priroda:obec-pujde-prikladem", + "priroda:udrzitelne-vyuzivani-obecni-zelene", + "priroda:pece-o-zelen", + "priroda:komunitni-pestitelstvi", + "priroda:hajime-zajmy-obyvatel", + "priroda:cista-moderni-nezavisla-energetika", + "priroda:obce-pripravene-na-oteplovani", + "priroda:kvalita-ovzdusi", + "priroda:kvalitni-verejne-stravovani", + "priroda:voda-je-dulezita", + "priroda:obnovitelne-zdroje", + "aktivni-verejnost:referendum-a-obcanske-iniciativy", + "aktivni-verejnost:pozornost-peticim", + "aktivni-verejnost:elektronicka-zpetna-vazba-a-diskuze", + "aktivni-verejnost:podpora-cinnosti-spolku", + "aktivni-verejnost:podpora-mistniho-podnikani", + "verejne-sluzby:ani-obec-neni-velkym-bratrem", + "verejne-sluzby:kvalitni-verejne-sluzby", + "socialni-politika:dostupnost-terennich-a-ambulantnich-sluzeb", + "socialni-politika:zajisteni-kvalitni-pece-v-zarizenich", + "socialni-politika:dostupnost-rozvoj-odlehcovacich-sluzeb", + "socialni-politika:dostupne-najemni-bydleni", + "socialni-politika:planovani-koordinace-v-socialni-oblasti", + "socialni-politika:zvyhodnena-nutna-doprava-senioru", + "socialni-politika:system-kvalitnich-sluzeb-pece-o-deti", + "socialni-politika:podpora-sluzeb-pro-ohrozene-deti", + "socialni-politika:podpora-mezigeneracniho-souziti", + "socialni-politika:prevence-rozpadu-rodin",{ + "SECRET_KEY": "odvazne delani neceho spravneho, nebo tak nejak", + "IDENTIFIER_HASH_PEPPER": "降り出した雨の音はずっと遠くまで響いた", + "IDENTIFIER_HASH_TIME_COST": 3, + "IDENTIFIER_HASH_MEMORY_COST": 65536, + "IDENTIFIER_HASH_PARALLELISM": 4, + "POINTS": [ + "bydleni:nerozprodame-byty", + "bydleni:transparentni-pravidla-s-bytovym-fondem", + "bydleni:pece-o-bytove-fondy", + "bydleni:spoluprace-s-ministerstvem-pro-mistni-rozvoj", + "bydleni:komunitni-bydleni", + "bydleni:dostupne-socialni-bydleni", + "bydleni:mapovani-potreb-lidi-v-obci", + "bydleni:vztah-k-developerum", + "bydleni:ferove-mistni-poplatky", + "bydleni:mmr-rekonstrukce-socialniho-bydleni", + "otevrena-radnice:jasne-informace-z-radnice", + "otevrena-radnice:transparentni-rozhodovani-ve-verejnem-zajmu", + "otevrena-radnice:pruhledne-hospodareni-s-majetkem", + "otevrena-radnice:hospodarny-transparentni-rozpocet", + "otevrena-radnice:verejna-zadavaci-dokumentace", + "otevrena-radnice:prime-zakazky-vyjimka", + "otevrena-radnice:zverejnovani-zakazek-mimo-prazdniny", + "otevrena-radnice:zadavaci-rizeni-u-opakovanych-zakazek", + "otevrena-radnice:dbat-na-projektove-dokumentace", + "otevrena-radnice:svobodny-software-a-formaty", + "otevrena-radnice:granty-a-dotace-pro-mesto", + "otevrena-radnice:odborni-zastupci-mesta-v-organech", + "otevrena-radnice:pravidla-pro-obce-i-jejich-organizace", + "otevrena-radnice:pravidla-pro-nominace-a-odmenovani-v-spolecnostech", + "otevrena-radnice:uzemni-plan", + "otevrena-radnice:mesta-bez-barier", + "otevrena-radnice:kvalitni-verejny-prostor", + "otevrena-radnice:ucast-na-rozhodovani", + "otevrena-radnice:radnicni-media", + "otevrena-radnice:nekale-jednani-a-stret-zajmu", + "priroda:obec-pujde-prikladem", + "priroda:udrzitelne-vyuzivani-obecni-zelene", + "priroda:pece-o-zelen", + "priroda:komunitni-pestitelstvi", + "priroda:hajime-zajmy-obyvatel", + "priroda:cista-moderni-nezavisla-energetika", + "priroda:obce-pripravene-na-oteplovani", + "priroda:kvalita-ovzdusi", + "priroda:kvalitni-verejne-stravovani", + "priroda:voda-je-dulezita", + "priroda:obnovitelne-zdroje", + "aktivni-verejnost:referendum-a-obcanske-iniciativy", + "aktivni-verejnost:pozornost-peticim", + "aktivni-verejnost:elektronicka-zpetna-vazba-a-diskuze", + "aktivni-verejnost:podpora-cinnosti-spolku", + "aktivni-verejnost:podpora-mistniho-podnikani", + "verejne-sluzby:ani-obec-neni-velkym-bratrem", + "verejne-sluzby:kvalitni-verejne-sluzby", + "socialni-politika:dostupnost-terennich-a-ambulantnich-sluzeb", + "socialni-politika:zajisteni-kvalitni-pece-v-zarizenich", + "socialni-politika:dostupnost-rozvoj-odlehcovacich-sluzeb", + "socialni-politika:dostupne-najemni-bydleni", + "socialni-politika:planovani-koordinace-v-socialni-oblasti", + "socialni-politika:zvyhodnena-nutna-doprava-senioru", + "socialni-politika:system-kvalitnich-sluzeb-pece-o-deti", + "socialni-politika:podpora-sluzeb-pro-ohrozene-deti", + "socialni-politika:podpora-mezigeneracniho-souziti", + "socialni-politika:prevence-rozpadu-rodin", + "socialni-politika:podpora-tymu-pro-znevyhodnene-rodiny", + "socialni-politika:podpora-treninkovych-pracovnich-mist", + "socialni-politika:podpora-pravidelne-supervize", + "socialni-politika:koncepcni-zakladani-nabytkovych-bank", + "socialni-politika:navykove-chovani", + "zdravotni-pece:dostupnost-pece", + "zdravotni-pece:kvalita-pece", + "doprava:financovani-verejne-dopravy", + "doprava:verejny-prostor-spolecensky-kompromis", + "doprava:svoboda-zpusobu-prepravy", + "doprava:kvalitni-moderni-mhd", + "doprava:propojovani-verejne-dopravy", + "vzdelavani:dostupnost-skol", + "vzdelavani:kvalita-skol", + "vzdelavani:kvalita-vyuky", + "vzdelavani:spoluprace-ucitelu", + "vzdelavani:klima-ve-skolach", + "vzdelavani:digitalizace-skol", + "vzdelavani:kvalitni-poradenske-sluzby", + "vzdelavani:karierove-poradesntvi-na-skolach", + "kultura:podpora-kultury-a-financovani", + "kultura:koncepcni-cinnost", + "kultura:modernizace-knihoven", + "kultura:aktualizace-pametovych-instituci", + "kultura:komunitni-projekty-a-volnocasove-aktivity", + "kultura:udrzovani-kulturnich-pamatek", + "cestovni-ruch:naucne-stezky-cyklostezky", + "cestovni-ruch:digitalizace-cestovniho-ruchu", + "sport:rekonstrukce-sportovist", + "sport:zdravy-zivotni-styl", + "sport:pohybova-gramotnost" + ] +} + + "socialni-politika:podpora-tymu-pro-znevyhodnene-rodiny", + "socialni-politika:podpora-treninkovych-pracovnich-mist", + "socialni-politika:podpora-pravidelne-supervize", + "socialni-politika:koncepcni-zakladani-nabytkovych-bank", + "socialni-politika:navykove-chovani", + "zdravotni-pece:dostupnost-pece", + "zdravotni-pece:kvalita-pece", + "doprava:financovani-verejne-dopravy", + "doprava:verejny-prostor-spolecensky-kompromis", + "doprava:svoboda-zpusobu-prepravy", + "doprava:kvalitni-moderni-mhd", + "doprava:propojovani-verejne-dopravy", + "vzdelavani:dostupnost-skol", + "vzdelavani:kvalita-skol", + "vzdelavani:kvalita-vyuky", + "vzdelavani:spoluprace-ucitelu", + "vzdelavani:klima-ve-skolach", + "vzdelavani:digitalizace-skol", + "vzdelavani:kvalitni-poradenske-sluzby", + "vzdelavani:karierove-poradesntvi-na-skolach", + "kultura:podpora-kultury-a-financovani", + "kultura:koncepcni-cinnost", + "kultura:modernizace-knihoven", + "kultura:aktualizace-pametovych-instituci", + "kultura:komunitni-projekty-a-volnocasove-aktivity", + "kultura:udrzovani-kulturnich-pamatek", + "cestovni-ruch:naucne-stezky-cyklostezky", + "cestovni-ruch:digitalizace-cestovniho-ruchu", + "rekonstrukce-sportovist", + "zdravy-zivotni-styl", + "pohybova-gramotnost" ] } diff --git a/measurer/__init__.py b/measurer/__init__.py index 0bb5c54..54ab024 100644 --- a/measurer/__init__.py +++ b/measurer/__init__.py @@ -39,7 +39,7 @@ def create_app() -> flask.Flask: app.logger.debug("Creating engine") - sa_engine = sqlalchemy.create_engine(app.config["DATABASE_URI"]) + sa_engine = sqlalchemy.create_engine(os.environ["DATABASE_URL"]) app.sa_session_class = sqlalchemy.orm.scoped_session( sqlalchemy.orm.sessionmaker( diff --git a/measurer/database/models.py b/measurer/database/models.py index 39948d3..5560268 100644 --- a/measurer/database/models.py +++ b/measurer/database/models.py @@ -5,6 +5,8 @@ import sqlalchemy from . import Base from .utils import UUID, get_uuid +from .. import utils + __all__ = ["Vote"] @@ -27,6 +29,7 @@ class Vote(Base): identifier = sqlalchemy.Column( sqlalchemy.String(64), + default=utils.get_ip_hash, nullable=False ) """An identifier of the person who made the vote. This should be, for example, diff --git a/measurer/static/simple.css b/measurer/static/simple.css deleted file mode 100644 index 377a6d1..0000000 --- a/measurer/static/simple.css +++ /dev/null @@ -1,516 +0,0 @@ -/* Global variables. */ -:root { - /* Set sans-serif & mono fonts */ - --sans-font: -apple-system, BlinkMacSystemFont, "Avenir Next", Avenir, - "Nimbus Sans L", Roboto, Noto, "Segoe UI", Arial, Helvetica, - "Helvetica Neue", sans-serif; - --mono-font: Consolas, Menlo, Monaco, "Andale Mono", "Ubuntu Mono", monospace; - - /* Default (light) theme */ - --bg: #fff; - --accent-bg: #000000; - --text: #8a8a8a; - --text-light: #585858; - --border: #d8dae1; - --accent: #737373; - --code: #d81b60; - --preformatted: #444; - --marked: #ffdd33; - --disabled: #efefef; -} - -/* Dark theme */ -@media (prefers-color-scheme: dark) { - :root { - --bg: #212121; - --accent-bg: #000000; - --text: #dcdcdc; - --text-light: #ababab; - --border: #666; - --accent: #ffb300; - --code: #f06292; - --preformatted: #ccc; - --disabled: #111; - } - /* Add a bit of transparancy so light media isn't so glaring in dark mode */ - img, - video { - opacity: 0.8; - } -} - -html { - /* Set the font globally */ - font-family: var(--sans-font); - scroll-behavior: smooth; -} - -/* Make the body a nice central block */ -body { - color: var(--text); - background: var(--bg); - font-size: 1.15rem; - line-height: 1.5; - display: grid; - grid-template-columns: - 1fr min(45rem, 90%) 1fr; - margin: 0; -} - -body>* { - grid-column: 2; -} - -/* Make the header bg full width, but the content inline with body */ -body > header { - background: var(--accent-bg); - color: var(--bg); - border-bottom: 1px solid var(--border); - text-align: center; - padding: 1rem; - grid-column: 1 / -1; - box-sizing: border-box; -} - -body > header h1 { - max-width: 1200px; - margin: 1rem auto; -} - -body > header p { - max-width: 40rem; - margin: 1rem auto; -} - -/* Add a little padding to ensure spacing is correct between content and nav */ -main { - padding-top: 1.5rem; -} - -body > footer { - margin-top: 4rem; - padding: 1.5rem 1rem 1.5rem 1rem; - color: var(--text-light); - font-size: 0.9rem; - text-align: center; - border-top: 1px solid var(--border); -} - -/* Format headers */ -h1 { - font-size: 3rem; -} - -h2 { - font-size: 2.6rem; - margin-top: 3rem; -} - -h3 { - font-size: 2rem; - margin-top: 3rem; -} - -h4 { - font-size: 1.44rem; -} - -h5 { - font-size: 1.15rem; -} - -h6 { - font-size: 0.96rem; -} - -/* Fix line height when title wraps */ -h1, -h2, -h3 { - line-height: 1.1; -} - -/* Reduce header size on mobile */ -@media only screen and (max-width: 720px) { - h1 { - font-size: 2.5rem; - } - - h2 { - font-size: 2.1rem; - } - - h3 { - font-size: 1.75rem; - } - - h4 { - font-size: 1.25rem; - } -} - -/* Format links & buttons */ -a, -a:visited { - color: var(--accent); -} - -a:hover { - text-decoration: none; -} - -button, -[role="button"], -input[type="submit"], -input[type="reset"], -input[type="button"] { - border: none; - border-radius: 5px; - background: var(--accent); - font-size: 1rem; - color: var(--bg); - padding: 0.7rem 0.9rem; - margin: 0.5rem 0; -} - -button[disabled], -[role="button"][aria-disabled="true"], -input[type="submit"][disabled], -input[type="reset"][disabled], -input[type="button"][disabled], -input[type="checkbox"][disabled], -input[type="radio"][disabled], -select[disabled] { - opacity: 0.5; - cursor: not-allowed; -} - -input:disabled, -textarea:disabled, -select:disabled { - cursor: not-allowed; - background-color: var(--disabled); -} - -input[type="range"] { - padding: 0; -} - -/* Set the cursor to '?' while hovering over an abbreviation */ -abbr { - cursor: help; -} - -button:focus, -button:enabled:hover, -[role="button"]:focus, -[role="button"]:not([aria-disabled="true"]):hover, -input[type="submit"]:focus, -input[type="submit"]:enabled:hover, -input[type="reset"]:focus, -input[type="reset"]:enabled:hover, -input[type="button"]:focus, -input[type="button"]:enabled:hover { - filter: brightness(1.4); - cursor: pointer; -} - -/* Format navigation */ -nav { - font-size: 1rem; - line-height: 2; - padding: 1rem 0 0 0; -} - -/* Use flexbox to allow items to wrap, as needed */ -nav ul, -nav ol { - align-content: space-around; - align-items: center; - display: flex; - flex-direction: row; - justify-content: center; - list-style-type: none; - margin: 0; - padding: 0; -} - -/* List items are inline elements, make them behave more like blocks */ -nav ul li, -nav ol li { - display: inline-block; -} - -nav a, -nav a:visited { - margin: 0 1rem 1rem 0; - border: 1px solid var(--border); - border-radius: 5px; - color: var(--text); - display: inline-block; - padding: 0.1rem 1rem; - text-decoration: none; -} - -nav a:hover { - color: var(--accent); - border-color: var(--accent); -} - -nav a:last-child { - margin-right: 0; -} - -/* Reduce nav side on mobile */ -@media only screen and (max-width: 750px) { - nav a { - border: none; - padding: 0; - color: var(--accent); - text-decoration: underline; - line-height: 1; - } -} - -/* Format the expanding box */ -details { - background: var(--accent-bg); - border: 1px solid var(--border); - border-radius: 5px; - margin-bottom: 1rem; -} - -summary { - cursor: pointer; - font-weight: bold; - padding: 0.6rem 1rem; -} - -details[open] { - padding: 0.6rem 1rem 0.75rem 1rem; -} - -details[open] summary + * { - margin-top: 0; -} - -details[open] summary { - margin-bottom: 0.5rem; - padding: 0; -} - -details[open] > *:last-child { - margin-bottom: 0; -} - -/* Format tables */ -table { - border-collapse: collapse; - width: 100%; - margin: 1.5rem 0; -} - -td, -th { - border: 1px solid var(--border); - text-align: left; - padding: 0.5rem; -} - -th { - background: var(--accent-bg); - font-weight: bold; -} - -tr:nth-child(even) { - /* Set every other cell slightly darker. Improves readability. */ - background: var(--accent-bg); -} - -table caption { - font-weight: bold; - margin-bottom: 0.5rem; -} - -/* Format forms */ -textarea, -select, -input { - font-size: inherit; - font-family: inherit; - padding: 0.5rem; - margin-bottom: 0.5rem; - color: var(--text); - background: var(--bg); - border: 1px solid var(--border); - border-radius: 5px; - box-shadow: none; - box-sizing: border-box; - width: 60%; - -moz-appearance: none; - -webkit-appearance: none; - appearance: none; -} - -/* Add arrow to drop-down */ -select { - background-image: linear-gradient(45deg, transparent 49%, var(--text) 51%), - linear-gradient(135deg, var(--text) 51%, transparent 49%); - background-position: calc(100% - 20px), calc(100% - 15px); - background-size: 5px 5px, 5px 5px; - background-repeat: no-repeat; -} - -select[multiple] { - background-image: none !important; -} - -/* checkbox and radio button style */ -input[type="checkbox"], -input[type="radio"] { - vertical-align: bottom; - position: relative; -} - -input[type="radio"] { - border-radius: 100%; -} - -input[type="checkbox"]:checked, -input[type="radio"]:checked { - background: var(--accent); -} - -input[type="checkbox"]:checked::after { - /* Creates a rectangle with colored right and bottom borders which is rotated to look like a check mark */ - content: " "; - width: 0.1em; - height: 0.25em; - border-radius: 0; - position: absolute; - top: 0.05em; - left: 0.18em; - background: transparent; - border-right: solid var(--bg) 0.08em; - border-bottom: solid var(--bg) 0.08em; - font-size: 1.8em; - transform: rotate(45deg); -} -input[type="radio"]:checked::after { - /* creates a colored circle for the checked radio button */ - content: " "; - width: 0.25em; - height: 0.25em; - border-radius: 100%; - position: absolute; - top: 0.125em; - background: var(--bg); - left: 0.125em; - font-size: 32px; -} - -/* Make the textarea wider than other inputs */ -textarea { - width: 80%; -} - -/* Makes input fields wider on smaller screens */ -@media only screen and (max-width: 720px) { - textarea, - select, - input { - width: 100%; - } -} - -/* Ensures the checkbox and radio inputs do not have a set width like other input fields */ -input[type="checkbox"], -input[type="radio"] { - width: auto; -} - -/* do not show border around file selector button */ -input[type="file"] { - border: 0; -} - -/* Misc body elements */ -hr { - color: var(--border); - border-top: 1px; - margin: 1rem auto; -} - -mark { - padding: 2px 5px; - border-radius: 4px; - background: var(--marked); -} - -main img, -main video { - max-width: 100%; - height: auto; - border-radius: 5px; -} - -figure { - margin: 0; - text-align: center; -} - -figcaption { - font-size: 0.9rem; - color: var(--text-light); - margin-bottom: 1rem; -} - -blockquote { - margin: 2rem 0 2rem 2rem; - padding: 0.4rem 0.8rem; - border-left: 0.35rem solid var(--accent); - color: var(--text-light); - font-style: italic; -} - -cite { - font-size: 0.9rem; - color: var(--text-light); - font-style: normal; -} - -/* Use mono font for code elements */ -code, -pre, -pre span, -kbd, -samp { - font-family: var(--mono-font); - color: var(--code); -} - -kbd { - color: var(--preformatted); - border: 1px solid var(--preformatted); - border-bottom: 3px solid var(--preformatted); - border-radius: 5px; - padding: 0.1rem 0.4rem; -} - -pre { - padding: 1rem 1.4rem; - max-width: 100%; - overflow: auto; - color: var(--preformatted); - background: var(--accent-bg); - border: 1px solid var(--border); - border-radius: 5px; -} - -/* Fix embedded code within pre */ -pre code { - color: var(--preformatted); - background: none; - margin: 0; - padding: 0; -} diff --git a/measurer/templates/base.html b/measurer/templates/base.html deleted file mode 100644 index 1ecc3c7..0000000 --- a/measurer/templates/base.html +++ /dev/null @@ -1,11 +0,0 @@ -<!DOCTYPE html> -<html> - <head> - <title>{% block current_page %}{% endblock %}</title> - <meta charset="utf-8"> - <link rel="stylesheet" href="{{ url_for('static', filename='simple.css') }}"> - </head> - <body> - {% block content %}{% endblock %} - </body> -</html> diff --git a/measurer/templates/racer_add.html b/measurer/templates/racer_add.html deleted file mode 100644 index d2b847c..0000000 --- a/measurer/templates/racer_add.html +++ /dev/null @@ -1,26 +0,0 @@ -{% extends 'base.html' %} - -{% block current_page %}{{ gettext("Add a racer") }}{% endblock %} - -{% block content %}<header> - <h1>{{ gettext("Add a racer") }}</h1> -</header> -<main> - <form method="POST"> - {{ form.hidden_tag() }} - - <div> - {{ form.number(placeholder=form.number.label.text) }} - </div> - - <div> - {{ form.name(placeholder=form.name.label.text) }} - </div> - - <div> - {{ form.time(placeholder=form.time.label.text) }} - </div> - - <input type="submit" value="{{ gettext('Add racer') }}"> - </form> -</main>{% endblock %} diff --git a/measurer/templates/racer_edit.html b/measurer/templates/racer_edit.html deleted file mode 100644 index d155bdf..0000000 --- a/measurer/templates/racer_edit.html +++ /dev/null @@ -1,26 +0,0 @@ -{% extends 'base.html' %} - -{% block current_page %}{{ gettext("Edit racer %s").format(racer.number) }}{% endblock %} - -{% block content %}<header> - <h1>{{ gettext("Edit racer %s").format(racer.number) }}</h1> -</header> -<main> - <form method="POST"> - {{ form.hidden_tag() }} - - <div> - {{ form.number(placeholder=form.number.label.text, value=racer.number) }} - </div> - - <div> - {{ form.name(placeholder=form.name.label.text, value=racer.name) }} - </div> - - <div> - {{ form.time(placeholder=form.time.label.text, value=racer.time.total_seconds()|int) }} - </div> - - <input type="submit" value="{{ gettext('Update racer') }}"> - </form> -</main>{% endblock %} diff --git a/measurer/templates/racer_list.html b/measurer/templates/racer_list.html deleted file mode 100644 index 54194b2..0000000 --- a/measurer/templates/racer_list.html +++ /dev/null @@ -1,46 +0,0 @@ -{% extends 'base.html' %} - -{% block current_page %}{{ gettext("Racers") }}{% endblock %} - -{% block content %}<header> - <h1>{{ gettext("Racers") }}</h1> -</header> -<main> - <a href="{{ url_for('racer.add') }}">{{ gettext("Add a racer") }}</a> - - <table> - <tr> - <th>{{ gettext("Number") }}</th> - <th>{{ gettext("Name") }}</th> - <th>{{ gettext("Time") }}</th> - </tr> - {% for racer in racers %} - <tr> - <td> - <a href="{{ url_for('racer.view', id_=racer.id) }}">{{ racer.number }}</a> - </td> - <td>{{ racer.name }}</td> - <td>{{ racer.time }}</td> - </tr> - {% endfor %} - </table> - - <form method="GET"> - <b>{{ gettext("Sort") }}</b> - - <hr> - - <select name="order-by"> - <option value="number" {% if order_by == 'number' %}selected{% endif %}>{{ gettext("Number") }}</option> - <option value="name" {% if order_by == 'name' %}selected{% endif %}>{{ gettext("Name") }}</option> - <option value="time" {% if order_by == 'time' %}selected{% endif %}>{{ gettext("Time") }}</option> - </select> - - <select name="order-type"> - <option value="asc" {% if order_type == 'asc' %}selected{% endif %}>{{ gettext("Ascending") }}</option> - <option value="desc" {% if order_type == 'desc' %}selected{% endif %}>{{ gettext("Descending") }}</option> - </select> - - <input type="submit" value="Sort, without JS!"> - </form> -</main>{% endblock %} diff --git a/measurer/templates/racer_view.html b/measurer/templates/racer_view.html deleted file mode 100644 index 3ea1427..0000000 --- a/measurer/templates/racer_view.html +++ /dev/null @@ -1,23 +0,0 @@ -{% extends 'base.html' %} - -{% block current_page %}{{ racer.name }}{% endblock %} - -{% block content %}<header> - <h1>{{ racer.name }}</h1> -</header> -<main> - <a href="{{ url_for('racer.list_') }}">{{ gettext("Back to listing") }}</a> - - <section> - <p> - {{ gettext("Number: ") }}{{ racer.number }}<br> - {{ gettext("Time: ") }}{{ racer.time }} - </p> - - <section> - <a href="{{ url_for('racer.delete', id_=racer.id) }}">{{ gettext("Delete this racer") }}</a> - {{ gettext(" or ") }} - <a href="{{ url_for('racer.edit', id_=racer.id) }}">{{ gettext("Update information") }}</a> - </section> - </section> -</main>{% endblock %} diff --git a/measurer/types/__init__.py b/measurer/types/__init__.py new file mode 100644 index 0000000..2e2c6ab --- /dev/null +++ b/measurer/types/__init__.py @@ -0,0 +1,21 @@ +"""Abstract classes for type hints.""" + +from __future__ import annotations + +import abc +import typing + +__all__ = ["SupportsLength"] +__version__ = "1.0.1" + + +@typing.runtime_checkable +class SupportsLength(typing.Protocol): + """An abstract class which supports the ``__len__`` method. + Adapted from the + `typing <https://github.com/python/cpython/blob/3.8/Lib/typing.py>`_ library. + """ + + @abc.abstractmethod + def __len__(self) -> int: + pass diff --git a/measurer/utils/__pycache__/__init__.cpython-310.pyc b/measurer/utils/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d2af4122126573faf2d610d4d3c51e7510a2392c GIT binary patch literal 1715 zcmd1j<>g{vU|{Gf-I8>Jm4V?gh=Yuo85kHG7#J9eO&Ay$QW&BbQW#U1au}l+!8B78 z6PRX>VoqUBXN+P=VM%38V@hF7VQyiGVoPBPX3%7-;tDOv%*iatOfA-X2{PO77I%7T zNqlBOd`4n%#x1s@)RNMoykw9RGUjGrU~mRm9mBxDP{Oc)v4km$Ig6!)HH$5aeE~-b z<3h#~)-28}t`sIo1{6NCBm-221<qoXWJqBHo4}pLlfvH1zL0SNFUX8w22IYyc4h_! z_td=9qQsKaVueJ7q{QM>GZWp^yyX0p)D#8BqV)Vcqs$Zqkb@P{@{1HoGEx<iON)w9 z^GX!lauSQP6^c>|OH+$W)Qh<|i&As*OHvgQQ&Ngji;Hy>$}>wcP;?{~6sQ*~BqVsc zxcY^7x_P<=#d|mgd&CF01_ZbUB_t>$=jWwmrYmHpR&sGB7K8N`C+3tWXcVLt6r>hu z>Tz*#2506ar-EISTC7l>nUkZCm{XouS*(zhig0jZUW!6-Mq*Ja)QsFzg@VMQ#N5=9 z)S_Y?g_3-Q;>5C4F3#eT{G!D4R2_xXijty4g_OjSM1|7g(!`vcN`-==)MAi7Gm91S z@=Fvl^OAE)L7@UNJ3lX{5^Qu)Vma6?3Yp*l0L5r(3dosOkmxP8QV7X_I2R<Urw1}C z)yj&ClhYC6!W0FF=M*xF^VCbgh9;#Jmnfta<>x9SDio(CmlkD~R4NpgB<7_Q<Y(rU z=qMzWA}K6S<>KT_PRvtCOiRlzN&$JUv^Z5EBef`1N1+%TS|u5o#R_SqdC4W2`FRSN z#R>&QnYoEYnK_jTrNyaWEoqr~DO{YKsTG;UC7F5Y3Pt(l#o+XiqL7~lb`>ZQs23{~ zr52awlql$e(?e=SVs1fBs*XZBC=r0-0}|z2oSempxv2`C0kGttr{D%oKbgfK2WBJ| z6r|=UB$g;7DiozA7U$<BCgr3m6c?nXrsybu<Gd^}r!=*gi<7e`F)t-Q7h+vvat6r7 zMGDD@d7wy1N(FJj0gfEBA^8f4W%-#Y3OT8X*&yG5(rs~Sa#3oDLUwAU4%k2NU{@$k z1tpG>RA>wpr=}+66z78@DIq*FJF_4)B{NY$S0MnL3KcYxiz*9B^3#hF3o<G-bwJ?* zQk9chT&!S|QBqP+Y^ASXo|&Bq64A>qO4pC!;^YJg=tE41M>WANA)bq~inX*PP1hny z414B?l0+8|@$_|#clHksiIPT_@^$s~4~oPg8Q>V?=;Pz+;~DJxQh|YiL6hwk3n;*& zI7>2fQ{$8Ki%X)ob5nEkiz=ZUP~ha`q~>H6=SFdWQbBx9YThlDlFEWqKTW<{Y>-mp z7H5)~38dt>#g+&zIifgo^2_6MQp-|vqWHlYA#o62lvsX?H4RjL++v3oAh)<7#Ya4- z__)OeHHaHp*u*Cm6x?Ei6c4vpLMjVVZ!vkg++s^f1(j4Q8H$7%7$C$iKmCmS+*JM2 z;?yGjf=c~@%%a4SOughP-GU-el;!Fc<QJ6YB!cpkZf<H~acNO%kv^PJT9TPltREkr znU`4-AFo$XS;WM^zyPW&ilrDB7+9p(7<m{47`ecZiIIbm<tNjBHZG=bn(VjO<Kt8E zljGxy*g=lr1Q8&OAo0YUocQ=#%*7=|VDk{pg|I{z7#MDG*yQG?l;)(`F@j2e9*}Je O984Tc9E@yCLQDYG84;WS literal 0 HcmV?d00001 diff --git a/measurer/validators/__init__.py b/measurer/validators/__init__.py new file mode 100644 index 0000000..492c9f9 --- /dev/null +++ b/measurer/validators/__init__.py @@ -0,0 +1,338 @@ +"""Validation using the Cerberus library. To extend, see its documentation +`here <https://docs.python-cerberus.org/en/stable/index.html>`_. +""" + +from __future__ import annotations + +import base64 +import datetime +import functools +import re +import typing +import uuid + +import cerberus +import flask +import werkzeug.exceptions + +import validators + +from .. import types + +__all__ = ["APIValidator", "validate_json"] +__version__ = "1.12.0" + + +class APIValidator(cerberus.Validator): + """Cerberus validator with extended functionality.""" + + def _check_with_is_base64( + self: APIValidator, + field: str, + value: typing.Union[None, str] + ) -> None: + """Checks whether or not ``value`` is a valid base64 string, without + coercing it into the bytes it represents. This is useful where external + tools require untouched base64 strings. + + :param field: The current field. + :param value: The current field's value. If :data:`None`, this means the + field is nullable. Nothing happens in this case. + """ + + if value is None: + return + + try: + base64.b64decode(value, validate=True) + except ValueError: + self._error( + field, + "must be a valid base64 string" + ) + + def _check_with_is_email( + self: APIValidator, + field: str, + value: typing.Union[None, str] + ) -> None: + """Checks whether or not ``value`` is a valid email address. + + :param field: The current field. + :param value: The current field's value. If :data:`None`, this means the + field is nullable. Nothing happens in this case. + """ + + if value is None: + return + + if not validators.email(value): + self._error( + field, + "must be a valid email address" + ) + + def _check_with_is_valid_regex( + self: APIValidator, + field: str, + value: typing.Union[None, str] + ) -> None: + """Checks whether or not ``value`` is a valid regular expression. + + :param field: The current field. + :param value: The current field's value. If :data:`None`, this means the + field is nullable. Nothing happens in this case. + """ + + if value is None: + return + + try: + re.compile(value) + except re.error: + self._error(field, "must be a valid regular expression") + + def _check_with_is_public_url( + self: APIValidator, + field: str, + value: typing.Union[None, str] + ) -> None: + """Checks that ``value`` is a valid URL. If + :attr:`debug <heiwa.ConfiguredLockFlask.debug>` mode is not enabled, the + URL must also correspond to a public resource. + + :param field: The current field. + :param value: The current field's value. If :data:`None`, this means the + field is nullable. Nothing happens in this case. + """ + + if value is None: + return + + if not validators.url( + value, + public=(not flask.current_app.debug) + ): + self._error( + field, + "must be a valid public URL" + ) + + def _check_with_has_no_duplicates( + self: APIValidator, + field: str, + value: typing.Union[None, typing.List[typing.Any]] + ) -> None: + """Checks that the list in ``value`` contains no duplicate items. + + :param field: The current field. + :param value: The current field's value. If :data:`None`, this means the + field is nullable. Nothing happens in this case. + """ + + if value is None: + return + + if len(value) != len(set(value)): + self._error( + field, + "must contain no duplicate items" + ) + + def _normalize_coerce_convert_to_uuid( + self: APIValidator, + value: typing.Union[None, str] + ) -> typing.Union[None, uuid.UUID]: + """Converts the ``value`` to an :class:`UUID <uuid.UUID>`. + + :param value: The current field's value. If :data:`None`, this means the + field is nullable. Nothing happens in this case. + + :returns: The converted UUID. + """ + + if value is None: + return None + + return uuid.UUID(value) + + def _normalize_coerce_convert_to_datetime( + self: APIValidator, + value: typing.Union[None, str] + ) -> typing.Union[None, datetime.datetime]: + """As long as ``value`` is a string formatted as per + `ISO-8601 <https://wikiless.org/wiki/ISO_8601>`_, it's converted to a + :class:`datetime <datetime.datetime>` object. + + :raises ValueError: Raised when the datetime does not have a specified + time zone. + + :param value: The current field's value. If :data:`None`, this means the + field is nullable. Nothing happens in this case. + + :returns: The converted date and time. + """ + + if value is None: + return None + + result = datetime.datetime.fromisoformat(value) + + if result.tzinfo is None: + raise ValueError("must have a timezone") + + return result + + def _normalize_coerce_decode_base64( + self: APIValidator, + value: typing.Union[None, str] + ) -> typing.Union[None, bytes]: + """Converts the base64-encoded ``value`` to the bytes it represents. + + :param value: The current field's value. If :data:`None`, this means the + field is nullable. Nothing happens in this case. + + :returns: The decoded bytes. + """ + + if value is None: + return None + + return base64.b64decode( + value, + validate=True + ) + + def _validate_length_divisible_by( + self: APIValidator, + divider: int, + field: str, + value: typing.Union[None, types.SupportsLength] + ) -> None: + """Checks whether or not the length of ``value`` is divisible by ``divider``. + If not, an error is raised. + + :param divider: The required divider. + :param field: The current field. + :param value: The current field's value, which must support the ``__len__`` + method. If :data:`None`, this means the field is nullable. Nothing + happens in this case. + + The rule's arguments are validated against this schema: + { + 'type': 'integer' + } + """ + + if value is None: + return + + if len(value) % divider != 0: + self._error( + field, + f"length must be divisible by {divider}" + ) + + def _validate_not_null_dependencies( + self: APIValidator, + dependencies: typing.Union[ + typing.Iterable[str], + str + ], + field: str, + value: typing.Any + ) -> None: + """The same as the ``dependencies`` rule (see `here https://docs.pytho` + n-cerberus.org/en/stable/validation-rules.html#dependencies`_), but the + specified elements must not be :data:`None`, instead of just being set. + The current field must also not be :data:`None`, otherwise nothing + happens. + + :param dependencies: The list of dependencies. Either a string containing + a single field, or an iterable containing multiple. This is done + mostly to preserve the original ``dependencies`` rule's behaviour. + :param field: The current field. + :param value: The current field's value. + + The rule's arguments are validated against this schema: + { + 'type': ['string', 'list'] + } + """ + + if value is None: + return + + if isinstance(dependencies, str): + dependency_list = (dependencies,) + else: + dependency_list = dependencies + + for dependency in dependency_list: + found_name, found_value = self._lookup_field(dependency) + + if found_value is not None: + continue + + if found_name is None: + found_name = dependency + + self._error( + field, + f"depends on {found_name} being set and not null" + ) + + types_mapping = cerberus.Validator.types_mapping.copy() + types_mapping["uuid"] = cerberus.TypeDefinition( + "uuid", + (uuid.UUID,), + () + ) + + +def validate_json( + schema: typing.Dict[ + str, + typing.Union[ + str, + typing.Dict + ] + ], + *args, + **kwargs +) -> typing.Callable: + """Checks JSON data sent in the :attr:`flask.request` against a Cerberus + schema. If the validation was sucessful, sets :attr:`flask.g.json` to the + document the validator outputs - in case there were any coercions or other + modiciations done by it. + + :param schema: The cerberus schema. + + :raises werkzeug.exceptions.BadRequest: Raised when the JSON is invalid. + """ + + def wrapper( + func: typing.Callable + ) -> typing.Callable: + @functools.wraps(func) + def decorator(*wrapped_args, **wrapped_kwargs) -> typing.Any: + if flask.request.json is None: + raise werkzeug.exceptions.BadRequest + + if not isinstance(flask.request.json, dict): + raise werkzeug.exceptions.BadRequest + + validator = APIValidator( + schema, + *args, + **kwargs + ) + + if not validator.validate(flask.request.json): + raise werkzeug.exceptions.BadRequest(validator.errors) + + flask.g.json = validator.document + + return func(*wrapped_args, **wrapped_kwargs) + return decorator + return wrapper diff --git a/measurer/views/measurer.py b/measurer/views/measurer.py new file mode 100644 index 0000000..f14734e --- /dev/null +++ b/measurer/views/measurer.py @@ -0,0 +1,63 @@ +import http.client +import typing + +import flask +import sqlalchemy +import werkzeug.exceptions + +from .. import database, utils, validators + + +measurer_blueprint = flask.Blueprint( + "measurer", + __name__ +) + + +@measurer_blueprint.route("/", methods=["POST"]) +@validators.validate_json({ + "point": { + "type": "string", + "allowed": flask.current_app.config["POINTS"], + "nullable": False + } +}) +def add_vote() -> typing.Union[flask.Response, int]: + vote_exists = flask.g.sa_session.execute( + sqlalchemy.select(database.Vote). + where( + sqlalchemy.and_( + database.Vote.point == flask.g.json["point"], + database.Vote.identifier == utils.get_ip_hash() + ) + ). + exists(). + select() + ).scalars().one() + + if vote_exists: + raise werkzeug.exceptions.Forbidden + + new_vote = database.Vote(point=flask.g.json["point"]) + flask.g.sa_session.add(new_vote) + flask.g.sa_session.commit() + + return flask.jsonify(None), http.client.NO_CONTENT + + +@measurer_blueprint.route("/", methods=["GET"]) +def list_votes() -> typing.Union[flask.Response, int]: + votes = flask.g.sa_session.execute( + sqlalchemy.select( + database.Vote.point, + sqlalchemy.func.count(database.Vote.point) + ). + group_by(database.Vote.point) + ).all() + + result = {} + + for vote in votes: + result[vote[0]] = vote[1] + + return flask.jsonify(result), http.client.OK diff --git a/measurer/views/racer.py b/measurer/views/racer.py deleted file mode 100644 index e69de29..0000000 -- GitLab