diff options
29 files changed, 1368 insertions, 404 deletions
@@ -59,7 +59,7 @@ schema, lives under `./python_openapi_client/`. - [x] Authorization (aka, roles) - Web Interface - [x] Migrate Python codebase - - [ ] Creation and editing of all entities + - [x] Creation and editing of all entities - Other - [x] Elasticsearch schema - [x] Basic logging diff --git a/proposals/20200729_toml_editing.md b/proposals/20200729_toml_editing.md new file mode 100644 index 00000000..bdb8c12f --- /dev/null +++ b/proposals/20200729_toml_editing.md @@ -0,0 +1,43 @@ + +status: implemented + +TOML Editing of Entity Metadata +=============================== + +Goal is to enable full-power editing through the web interface, of the raw +entity schema and "extra" metadata, for all entity types. + +A side-effect of this should be enabling redirect editing between entities, as +well as "undeleting" or other state transitions (other than deletion). + +Plan: + +- find and add a toml transform library to pipenv deps. preferably with good + human-readable parsing errors +- TOML/JSON transform helpers, with tests +- implement generic TOML entity editing view (HTML) +- implement generic TOML entity editing endpoints + +Some metadata fields are removed before displaying TOML to edit. For example, +the ident, revision, and redirect fields for 'active' entities. It should still +be possible to do redirects by entering only the redirect field in the TOML +form. + +## UI Integration + +For existing edit forms, add a link to the "advanced" editing option. + +For endpoints without a form-based option (yet), do an HTTP redirect to the +TOML editing option. + +## New Webface Views + +`/<entity>/create/toml` + GET: display template to be filled in + POST: form submit for creation +`/<entity>/<ident>/edit/toml` + GET: transform entity for editing + POST: form submit for edit +`/editgroup/<editgroup_id>/<entity>/<ident>/edit/toml` + GET: transform entity for editing + POST: form submit for edit diff --git a/python/Pipfile b/python/Pipfile index 1c7ebf3a..4c0977a1 100644 --- a/python/Pipfile +++ b/python/Pipfile @@ -30,7 +30,7 @@ flask-wtf = "*" Flask-Misaka = "*" flask-mwoauth = "*" WTForms = "*" -loginpass = ">=0.4" +loginpass = "==0.4" # loginpass 0.4 is not actually compatible with newer authlib authlib = "<0.13" requests = ">=2" @@ -53,8 +53,9 @@ elasticsearch = ">=6.0.0,<7.0.0" dateparser = ">=0.7" langdetect = "*" pathlib2 = "*" -pycountry = "*" +pycountry = "==19.8.18" tldextract = "*" +toml = ">=0.10" [requires] # We install Python 3.7 using a PPA (deadsnakes) on Internet Archive cluster diff --git a/python/Pipfile.lock b/python/Pipfile.lock index 2f428b1c..9b1a5d15 100644 --- a/python/Pipfile.lock +++ b/python/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "94b31d68f1fdf85834841d90948ba599ba1a69723b0dd899f8ba9e709bc5307c" + "sha256": "b2e4ed259ddec6c4016e6857ec856146c0fb279b3d6a91d138c1611e9a591c3a" }, "pipfile-spec": 6, "requires": { @@ -47,43 +47,43 @@ }, "certifi": { "hashes": [ - "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304", - "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519" + "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", + "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" ], - "version": "==2020.4.5.1" + "version": "==2020.6.20" }, "cffi": { "hashes": [ - "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff", - "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b", - "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac", - "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0", - "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384", - "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26", - "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6", - "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b", - "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e", - "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd", - "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2", - "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66", - "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc", - "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8", - "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55", - "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4", - "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5", - "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d", - "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78", - "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa", - "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793", - "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f", - "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a", - "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f", - "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30", - "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f", - "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3", - "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c" - ], - "version": "==1.14.0" + "sha256:267adcf6e68d77ba154334a3e4fc921b8e63cbb38ca00d33d40655d4228502bc", + "sha256:26f33e8f6a70c255767e3c3f957ccafc7f1f706b966e110b855bfe944511f1f9", + "sha256:3cd2c044517f38d1b577f05927fb9729d3396f1d44d0c659a445599e79519792", + "sha256:4a03416915b82b81af5502459a8a9dd62a3c299b295dcdf470877cb948d655f2", + "sha256:4ce1e995aeecf7cc32380bc11598bfdfa017d592259d5da00fc7ded11e61d022", + "sha256:4f53e4128c81ca3212ff4cf097c797ab44646a40b42ec02a891155cd7a2ba4d8", + "sha256:4fa72a52a906425416f41738728268072d5acfd48cbe7796af07a923236bcf96", + "sha256:66dd45eb9530e3dde8f7c009f84568bc7cac489b93d04ac86e3111fb46e470c2", + "sha256:6923d077d9ae9e8bacbdb1c07ae78405a9306c8fd1af13bfa06ca891095eb995", + "sha256:833401b15de1bb92791d7b6fb353d4af60dc688eaa521bd97203dcd2d124a7c1", + "sha256:8416ed88ddc057bab0526d4e4e9f3660f614ac2394b5e019a628cdfff3733849", + "sha256:892daa86384994fdf4856cb43c93f40cbe80f7f95bb5da94971b39c7f54b3a9c", + "sha256:98be759efdb5e5fa161e46d404f4e0ce388e72fbf7d9baf010aff16689e22abe", + "sha256:a6d28e7f14ecf3b2ad67c4f106841218c8ab12a0683b1528534a6c87d2307af3", + "sha256:b1d6ebc891607e71fd9da71688fcf332a6630b7f5b7f5549e6e631821c0e5d90", + "sha256:b2a2b0d276a136146e012154baefaea2758ef1f56ae9f4e01c612b0831e0bd2f", + "sha256:b87dfa9f10a470eee7f24234a37d1d5f51e5f5fa9eeffda7c282e2b8f5162eb1", + "sha256:bac0d6f7728a9cc3c1e06d4fcbac12aaa70e9379b3025b27ec1226f0e2d404cf", + "sha256:c991112622baee0ae4d55c008380c32ecfd0ad417bcd0417ba432e6ba7328caa", + "sha256:cda422d54ee7905bfc53ee6915ab68fe7b230cacf581110df4272ee10462aadc", + "sha256:d3148b6ba3923c5850ea197a91a42683f946dba7e8eb82dfa211ab7e708de939", + "sha256:d6033b4ffa34ef70f0b8086fd4c3df4bf801fee485a8a7d4519399818351aa8e", + "sha256:ddff0b2bd7edcc8c82d1adde6dbbf5e60d57ce985402541cd2985c27f7bec2a0", + "sha256:e23cb7f1d8e0f93addf0cae3c5b6f00324cccb4a7949ee558d7b6ca973ab8ae9", + "sha256:effd2ba52cee4ceff1a77f20d2a9f9bf8d50353c854a282b8760ac15b9833168", + "sha256:f90c2267101010de42f7273c94a1f026e56cbc043f9330acd8a80e64300aba33", + "sha256:f960375e9823ae6a07072ff7f8a85954e5a6434f97869f50d0e41649a1c8144f", + "sha256:fcf32bf76dc25e30ed793145a57426064520890d7c02866eb93d3e4abe516948" + ], + "version": "==1.14.1" }, "chardet": { "hashes": [ @@ -113,71 +113,72 @@ "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==7.1.2" }, "confluent-kafka": { "hashes": [ - "sha256:1b10a9e4ede8c7ee382c16075b55275963d3fe9b8eec3fc511d0868847cc6eed", - "sha256:1c46cbc2eb0876f0cdbd33ed7ea684ed1b009a25b65cf87736d3506d2f4ae57e", - "sha256:2500a78334d642e49b98710722e548c0e3d5dc4c6eae63f02d66448678ed2922", - "sha256:2515771b18d190df2182881abcf02fe8fde0aab567402ff36295b35cd495de65", - "sha256:3150c8875511e2cea4086206f3c10448f744c9c35f9033fd0874c8c55f7b87e2", - "sha256:4b0a3c47f9183570e9ee77ae8c36080fbc1996045251e25772944e4dadf1db21", - "sha256:4f875798bbc766767b9c6ed95b084fde851e0bf074527ab0daffa87f4e750635", - "sha256:515049659b045b99e0464d5ff5def4785478490563bc5ac1341a4f29dc335e82", - "sha256:52088adf1abdf3a384a54ec7a3bfaa0b61e5da8cc03a2e26a8351bbbf49f72a9", - "sha256:5342d3ff348b8082eaa4c63f4c82a72f3bf0ef8efa12a8580c890fa6e160f761", - "sha256:55734905c5a8642e596cf1e60ec4d86f05d31a185cbc71d1c73430bb0c08db19", - "sha256:624349587e97135996383c58edd8d53b38c57d653e6536c1f816049fc75faea3", - "sha256:804a7d71b3cb61444930af67986064c9555b8c33f05a27003ea314d6c847e522", - "sha256:931231853cec933addfafa27772177dcfab899d82e2e39fe7485c0602088daf7", - "sha256:a4f5edc1d7958bbf5f12ba83c1f83e22a66daa9c4318c7f28c5bb1db9289fe09", - "sha256:a591936a90095144451f041315239b2c823b7a15fa820cf45e45c422591345d6", - "sha256:a6eb8f3f553e98a6ef0d00f9cf8e4e8dde73c914a43a00fecef97330de80bcea", - "sha256:aa48215edcf16071d44ba29951c82c5f541d5ec915590aff0b4240e8e13f3ba3", - "sha256:bfacb9fa0e3a5e31a5ac9a5da15de656e95e7153e022ec5620095b76a6098ec0", - "sha256:bfbcbe7068690369ac2de3fe953854de34ad5e901157e96bcb990ca8b86d1d93", - "sha256:c2660807e5c1ecd723e280f76918794c3fd84595000c1e8de1f254f5d89a785c", - "sha256:c42ff838ee5e248f95f65b5adca4e2fdd4a2817fa26cede36d83a426e0f1370c", - "sha256:c5b741764d8ea2b8334fdaf4b56297c5bab780142f1c0cad0bd642cac30cb89e", - "sha256:dac33a04f73093de275953867a05de244560aa9842def6316cbb52bc0f02eff3", - "sha256:f1695a00789795f9f798588bb62688b563baf471a76ca20fa01c957844938d7d", - "sha256:f25836e03559a381ba74b9a6940b716e61ba8ae2db2d5d3a40accbc60617e1af" + "sha256:00acc73f7d49961bf427f5e4fd6c0a220a6bfa5ccc91e0ad1f9ffa1751a169b0", + "sha256:0a59afbb90bdd22b9acdd3bb134f5ee1dff3cc5df55eaf52bf97b2f8d0d00de3", + "sha256:13b0e2011560f461ff39daf38089dd7f91404b3e66dba0456ccce0700f93c4f2", + "sha256:175c7064c8f19975616974558c45f42c147a202d4b1c0b0a83afefb920367696", + "sha256:22d7201d1aa89f1c5546749e781492925ed3eb0d7bd8f781fc57294cd45ddde3", + "sha256:3034cacc3b0d03eb3ce39cc5a64c1070d223870246f5d90c9113996be9db7df8", + "sha256:3e2d4f55ca952aeada3831d6615dc13a8a42c8e97175855ca08bbc6e6091b080", + "sha256:5a1c47320d6afc5b2599f8f8e143aed6845a2d903facde984606e02f10f11221", + "sha256:7b03bd9cc7b5e4df0a27eed359762c61a35313d4981ef1d9b418069eee454e66", + "sha256:85ff4823770ce2efaabb46d88e5ae26a840e0051fd481abaa805f21a5a84d003", + "sha256:9534cd2c0313df75b70eb4cf729382998970d97bbdda5cf3aef7081b855ccebe", + "sha256:99b13d0957a5967c85aee6138ef5f9acec90294267a549c5683744f20cf5d7b4", + "sha256:9a1c77291c1ac4b991aa0358f2f44636686eb8f52fb628502d30c312160a14e9", + "sha256:9c47b8aacfe347bffd86bf75b98626718912b63df87f256dff1abc06a0355410", + "sha256:a116382ae67e0d6a54684bab4ee9b1be54e789d031a6e5e74c3edc657c79d23c", + "sha256:b1c89f3653385acc5da71570e03281f35ac6960367f2b2a426ae431deb1a1a35", + "sha256:bb77276d569f511abe4a5b32a53f8a30285bc7be68219e5711a44720bf356ac2", + "sha256:bbd9633552840ab9367fb762ea21272759db8caec2c34ff16ee28be177644cdf", + "sha256:bfdfa81e4e72d2c24e408a5e199aae0a477499ae40647dfa6906d002d9b07f38", + "sha256:c7461d6db081c23a6d38ceba348e7c178d7e974cf22c45ba8a4918ecb8855a44", + "sha256:d6a5d4c72360a75e875e88f7cce42b66a786d037ca2002303ab1c580d49caf53", + "sha256:dabed41cc60d1fc6d3cb44a90fe02e5192c9bf0f73c7b35761981e62ecabc592", + "sha256:dd544847c713eeeb525031348ff6ffea4ecdd11c13590893e599a9d4676a9bd4", + "sha256:eba169a9de8c978c9f33c763857c5279eceac46a4fd55a381c2528b9d4b3359e", + "sha256:f2d1ee0bfdf618017bbfaa42406546155c1a86263e4f286295318578c723803b" ], "index": "pypi", - "version": "==1.4.2" + "version": "==1.5.0" }, "cryptography": { "hashes": [ - "sha256:091d31c42f444c6f519485ed528d8b451d1a0c7bf30e8ca583a0cac44b8a0df6", - "sha256:18452582a3c85b96014b45686af264563e3e5d99d226589f057ace56196ec78b", - "sha256:1dfa985f62b137909496e7fc182dac687206d8d089dd03eaeb28ae16eec8e7d5", - "sha256:1e4014639d3d73fbc5ceff206049c5a9a849cefd106a49fa7aaaa25cc0ce35cf", - "sha256:22e91636a51170df0ae4dcbd250d318fd28c9f491c4e50b625a49964b24fe46e", - "sha256:3b3eba865ea2754738616f87292b7f29448aec342a7c720956f8083d252bf28b", - "sha256:651448cd2e3a6bc2bb76c3663785133c40d5e1a8c1a9c5429e4354201c6024ae", - "sha256:726086c17f94747cedbee6efa77e99ae170caebeb1116353c6cf0ab67ea6829b", - "sha256:844a76bc04472e5135b909da6aed84360f522ff5dfa47f93e3dd2a0b84a89fa0", - "sha256:88c881dd5a147e08d1bdcf2315c04972381d026cdb803325c03fe2b4a8ed858b", - "sha256:96c080ae7118c10fcbe6229ab43eb8b090fccd31a09ef55f83f690d1ef619a1d", - "sha256:a0c30272fb4ddda5f5ffc1089d7405b7a71b0b0f51993cb4e5dbb4590b2fc229", - "sha256:bb1f0281887d89617b4c68e8db9a2c42b9efebf2702a3c5bf70599421a8623e3", - "sha256:c447cf087cf2dbddc1add6987bbe2f767ed5317adb2d08af940db517dd704365", - "sha256:c4fd17d92e9d55b84707f4fd09992081ba872d1a0c610c109c18e062e06a2e55", - "sha256:d0d5aeaedd29be304848f1c5059074a740fa9f6f26b84c5b63e8b29e73dfc270", - "sha256:daf54a4b07d67ad437ff239c8a4080cfd1cc7213df57d33c97de7b4738048d5e", - "sha256:e993468c859d084d5579e2ebee101de8f5a27ce8e2159959b6673b418fd8c785", - "sha256:f118a95c7480f5be0df8afeb9a11bd199aa20afab7a96bcf20409b411a3a85f0" - ], - "version": "==2.9.2" + "sha256:0c608ff4d4adad9e39b5057de43657515c7da1ccb1807c3a27d4cf31fc923b4b", + "sha256:0cbfed8ea74631fe4de00630f4bb592dad564d57f73150d6f6796a24e76c76cd", + "sha256:124af7255ffc8e964d9ff26971b3a6153e1a8a220b9a685dc407976ecb27a06a", + "sha256:384d7c681b1ab904fff3400a6909261cae1d0939cc483a68bdedab282fb89a07", + "sha256:45741f5499150593178fc98d2c1a9c6722df88b99c821ad6ae298eff0ba1ae71", + "sha256:4b9303507254ccb1181d1803a2080a798910ba89b1a3c9f53639885c90f7a756", + "sha256:4d355f2aee4a29063c10164b032d9fa8a82e2c30768737a2fd56d256146ad559", + "sha256:51e40123083d2f946794f9fe4adeeee2922b581fa3602128ce85ff813d85b81f", + "sha256:8713ddb888119b0d2a1462357d5946b8911be01ddbf31451e1d07eaa5077a261", + "sha256:8e924dbc025206e97756e8903039662aa58aa9ba357d8e1d8fc29e3092322053", + "sha256:8ecef21ac982aa78309bb6f092d1677812927e8b5ef204a10c326fc29f1367e2", + "sha256:8ecf9400d0893836ff41b6f977a33972145a855b6efeb605b49ee273c5e6469f", + "sha256:9367d00e14dee8d02134c6c9524bb4bd39d4c162456343d07191e2a0b5ec8b3b", + "sha256:a09fd9c1cca9a46b6ad4bea0a1f86ab1de3c0c932364dbcf9a6c2a5eeb44fa77", + "sha256:ab49edd5bea8d8b39a44b3db618e4783ef84c19c8b47286bf05dfdb3efb01c83", + "sha256:bea0b0468f89cdea625bb3f692cd7a4222d80a6bdafd6fb923963f2b9da0e15f", + "sha256:bec7568c6970b865f2bcebbe84d547c52bb2abadf74cefce396ba07571109c67", + "sha256:ce82cc06588e5cbc2a7df3c8a9c778f2cb722f56835a23a68b5a7264726bb00c", + "sha256:dea0ba7fe6f9461d244679efa968d215ea1f989b9c1957d7f10c21e5c7c09ad6" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==3.0" }, "dateparser": { "hashes": [ - "sha256:1b1f0e3034f82d1f92b45fa445826da6a36d67af8a1169e04869685594276011", - "sha256:fb5bfde4795fa4b179fe05c2c25b3981f785de26bec37e247dee1079c63d5689" + "sha256:7552c994f893b5cb8fcf103b4cd2ff7f57aab9bfd2619fdf0cf571c0740fd90b", + "sha256:e875efd8c57c85c2d02b238239878db59ff1971f5a823457fcc69e493bf6ebfa" ], "index": "pypi", - "version": "==0.7.4" + "version": "==0.7.6" }, "elasticsearch": { "hashes": [ @@ -196,7 +197,8 @@ "version": "==6.4.0" }, "fatcat-openapi-client": { - "path": "./../python_openapi_client" + "path": "./../python_openapi_client", + "version": "==0.3.2" }, "flask": { "hashes": [ @@ -255,29 +257,32 @@ }, "ftfy": { "hashes": [ - "sha256:67f9c8b33a4b742376a3eda11b0e3bd5c0cbe719d95ea0bfd3736a7bdd1c24c8" + "sha256:51c7767f8c4b47d291fcef30b9625fb5341c06a31e6a3b627039c706c42f3720" ], "index": "pypi", - "version": "==5.7" + "version": "==5.8" }, "future": { "hashes": [ "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.18.2" }, "idna": { "hashes": [ - "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", - "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "version": "==2.9" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" }, "itsdangerous": { "hashes": [ "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.1.0" }, "jinja2": { @@ -285,6 +290,7 @@ "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==2.11.2" }, "kazoo": { @@ -312,35 +318,40 @@ }, "lxml": { "hashes": [ - "sha256:06748c7192eab0f48e3d35a7adae609a329c6257495d5e53878003660dc0fec6", - "sha256:0790ddca3f825dd914978c94c2545dbea5f56f008b050e835403714babe62a5f", - "sha256:1aa7a6197c1cdd65d974f3e4953764eee3d9c7b67e3966616b41fab7f8f516b7", - "sha256:22c6d34fdb0e65d5f782a4d1a1edb52e0a8365858dafb1c08cb1d16546cf0786", - "sha256:2754d4406438c83144f9ffd3628bbe2dcc6d62b20dbc5c1ec4bc4385e5d44b42", - "sha256:27ee0faf8077c7c1a589573b1450743011117f1aa1a91d5ae776bbc5ca6070f2", - "sha256:2b02c106709466a93ed424454ce4c970791c486d5fcdf52b0d822a7e29789626", - "sha256:2d1ddce96cf15f1254a68dba6935e6e0f1fe39247de631c115e84dd404a6f031", - "sha256:4f282737d187ae723b2633856085c31ae5d4d432968b7f3f478a48a54835f5c4", - "sha256:51bb4edeb36d24ec97eb3e6a6007be128b720114f9a875d6b370317d62ac80b9", - "sha256:7eee37c1b9815e6505847aa5e68f192e8a1b730c5c7ead39ff317fde9ce29448", - "sha256:7fd88cb91a470b383aafad554c3fe1ccf6dfb2456ff0e84b95335d582a799804", - "sha256:9144ce36ca0824b29ebc2e02ca186e54040ebb224292072250467190fb613b96", - "sha256:925baf6ff1ef2c45169f548cc85204433e061360bfa7d01e1be7ae38bef73194", - "sha256:a636346c6c0e1092ffc202d97ec1843a75937d8c98aaf6771348ad6422e44bb0", - "sha256:a87dbee7ad9dce3aaefada2081843caf08a44a8f52e03e0a4cc5819f8398f2f4", - "sha256:a9e3b8011388e7e373565daa5e92f6c9cb844790dc18e43073212bb3e76f7007", - "sha256:afb53edf1046599991fb4a7d03e601ab5f5422a5435c47ee6ba91ec3b61416a6", - "sha256:b26719890c79a1dae7d53acac5f089d66fd8cc68a81f4e4bd355e45470dc25e1", - "sha256:b7462cdab6fffcda853338e1741ce99706cdf880d921b5a769202ea7b94e8528", - "sha256:b77975465234ff49fdad871c08aa747aae06f5e5be62866595057c43f8d2f62c", - "sha256:c47a8a5d00060122ca5908909478abce7bbf62d812e3fc35c6c802df8fb01fe7", - "sha256:c79e5debbe092e3c93ca4aee44c9a7631bdd407b2871cb541b979fd350bbbc29", - "sha256:d8d40e0121ca1606aa9e78c28a3a7d88a05c06b3ca61630242cded87d8ce55fa", - "sha256:ee2be8b8f72a2772e72ab926a3bccebf47bb727bda41ae070dc91d1fb759b726", - "sha256:f95d28193c3863132b1f55c1056036bf580b5a488d908f7d22a04ace8935a3a9", - "sha256:fadd2a63a2bfd7fb604508e553d1cf68eca250b2fbdbd81213b5f6f2fbf23529" - ], - "version": "==4.5.1" + "sha256:05a444b207901a68a6526948c7cc8f9fe6d6f24c70781488e32fd74ff5996e3f", + "sha256:08fc93257dcfe9542c0a6883a25ba4971d78297f63d7a5a26ffa34861ca78730", + "sha256:107781b213cf7201ec3806555657ccda67b1fccc4261fb889ef7fc56976db81f", + "sha256:121b665b04083a1e85ff1f5243d4a93aa1aaba281bc12ea334d5a187278ceaf1", + "sha256:1fa21263c3aba2b76fd7c45713d4428dbcc7644d73dcf0650e9d344e433741b3", + "sha256:2b30aa2bcff8e958cd85d907d5109820b01ac511eae5b460803430a7404e34d7", + "sha256:4b4a111bcf4b9c948e020fd207f915c24a6de3f1adc7682a2d92660eb4e84f1a", + "sha256:5591c4164755778e29e69b86e425880f852464a21c7bb53c7ea453bbe2633bbe", + "sha256:59daa84aef650b11bccd18f99f64bfe44b9f14a08a28259959d33676554065a1", + "sha256:5a9c8d11aa2c8f8b6043d845927a51eb9102eb558e3f936df494e96393f5fd3e", + "sha256:5dd20538a60c4cc9a077d3b715bb42307239fcd25ef1ca7286775f95e9e9a46d", + "sha256:74f48ec98430e06c1fa8949b49ebdd8d27ceb9df8d3d1c92e1fdc2773f003f20", + "sha256:786aad2aa20de3dbff21aab86b2fb6a7be68064cbbc0219bde414d3a30aa47ae", + "sha256:7ad7906e098ccd30d8f7068030a0b16668ab8aa5cda6fcd5146d8d20cbaa71b5", + "sha256:80a38b188d20c0524fe8959c8ce770a8fdf0e617c6912d23fc97c68301bb9aba", + "sha256:8f0ec6b9b3832e0bd1d57af41f9238ea7709bbd7271f639024f2fc9d3bb01293", + "sha256:92282c83547a9add85ad658143c76a64a8d339028926d7dc1998ca029c88ea6a", + "sha256:94150231f1e90c9595ccc80d7d2006c61f90a5995db82bccbca7944fd457f0f6", + "sha256:9dc9006dcc47e00a8a6a029eb035c8f696ad38e40a27d073a003d7d1443f5d88", + "sha256:a76979f728dd845655026ab991df25d26379a1a8fc1e9e68e25c7eda43004bed", + "sha256:aa8eba3db3d8761db161003e2d0586608092e217151d7458206e243be5a43843", + "sha256:bea760a63ce9bba566c23f726d72b3c0250e2fa2569909e2d83cda1534c79443", + "sha256:c3f511a3c58676147c277eff0224c061dd5a6a8e1373572ac817ac6324f1b1e0", + "sha256:c9d317efde4bafbc1561509bfa8a23c5cab66c44d49ab5b63ff690f5159b2304", + "sha256:cc411ad324a4486b142c41d9b2b6a722c534096963688d879ea6fa8a35028258", + "sha256:cdc13a1682b2a6241080745b1953719e7fe0850b40a5c71ca574f090a1391df6", + "sha256:cfd7c5dd3c35c19cec59c63df9571c67c6d6e5c92e0fe63517920e97f61106d1", + "sha256:e1cacf4796b20865789083252186ce9dc6cc59eca0c2e79cca332bdff24ac481", + "sha256:e70d4e467e243455492f5de463b72151cc400710ac03a0678206a5f27e79ddef", + "sha256:ecc930ae559ea8a43377e8b60ca6f8d61ac532fc57efb915d899de4a67928efd", + "sha256:f161af26f596131b63b236372e4ce40f3167c1b5b5d459b29d2514bd8c9dc9ee" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==4.5.2" }, "markupsafe": { "hashes": [ @@ -378,6 +389,7 @@ "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.1.1" }, "misaka": { @@ -399,6 +411,7 @@ "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.1.0" }, "pathlib2": { @@ -421,6 +434,7 @@ "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.20" }, "pygal": { @@ -448,10 +462,10 @@ }, "pylatexenc": { "hashes": [ - "sha256:bf15f2de2159501c9aa753929b5e0bec9aede4bfecb3b355f0bba8868eba0d1a" + "sha256:cd51da5ed908251c600b3f27fc1a8690727ba1e5f0b65f613aea1aca91c2530b" ], "index": "pypi", - "version": "==2.4" + "version": "==2.6" }, "pymacaroons": { "hashes": [ @@ -463,29 +477,25 @@ }, "pynacl": { "hashes": [ - "sha256:05c26f93964373fc0abe332676cb6735f0ecad27711035b9472751faa8521255", - "sha256:0c6100edd16fefd1557da078c7a31e7b7d7a52ce39fdca2bec29d4f7b6e7600c", - "sha256:0d0a8171a68edf51add1e73d2159c4bc19fc0718e79dec51166e940856c2f28e", - "sha256:1c780712b206317a746ace34c209b8c29dbfd841dfbc02aa27f2084dd3db77ae", - "sha256:2424c8b9f41aa65bbdbd7a64e73a7450ebb4aa9ddedc6a081e7afcc4c97f7621", - "sha256:2d23c04e8d709444220557ae48ed01f3f1086439f12dbf11976e849a4926db56", - "sha256:30f36a9c70450c7878053fa1344aca0145fd47d845270b43a7ee9192a051bf39", - "sha256:37aa336a317209f1bb099ad177fef0da45be36a2aa664507c5d72015f956c310", - "sha256:4943decfc5b905748f0756fdd99d4f9498d7064815c4cf3643820c9028b711d1", - "sha256:53126cd91356342dcae7e209f840212a58dcf1177ad52c1d938d428eebc9fee5", - "sha256:57ef38a65056e7800859e5ba9e6091053cd06e1038983016effaffe0efcd594a", - "sha256:5bd61e9b44c543016ce1f6aef48606280e45f892a928ca7068fba30021e9b786", - "sha256:6482d3017a0c0327a49dddc8bd1074cc730d45db2ccb09c3bac1f8f32d1eb61b", - "sha256:7d3ce02c0784b7cbcc771a2da6ea51f87e8716004512493a2b69016326301c3b", - "sha256:a14e499c0f5955dcc3991f785f3f8e2130ed504fa3a7f44009ff458ad6bdd17f", - "sha256:a39f54ccbcd2757d1d63b0ec00a00980c0b382c62865b61a505163943624ab20", - "sha256:aabb0c5232910a20eec8563503c153a8e78bbf5459490c49ab31f6adf3f3a415", - "sha256:bd4ecb473a96ad0f90c20acba4f0bf0df91a4e03a1f4dd6a4bdc9ca75aa3a715", - "sha256:bf459128feb543cfca16a95f8da31e2e65e4c5257d2f3dfa8c0c1031139c9c92", - "sha256:e2da3c13307eac601f3de04887624939aca8ee3c9488a0bb0eca4fb9401fc6b1", - "sha256:f67814c38162f4deb31f68d590771a29d5ae3b1bd64b75cf232308e5c74777e0" - ], - "version": "==1.3.0" + "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4", + "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4", + "sha256:2fe0fc5a2480361dcaf4e6e7cea00e078fcda07ba45f811b167e3f99e8cff574", + "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d", + "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25", + "sha256:537a7ccbea22905a0ab36ea58577b39d1fa9b1884869d173b5cf111f006f689f", + "sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505", + "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122", + "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7", + "sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420", + "sha256:8122ba5f2a2169ca5da936b2e5a511740ffb73979381b4229d9188f6dcb22f1f", + "sha256:9c4a7ea4fb81536c1b1f5cc44d54a296f96ae78c1ebd2311bd0b60be45a48d96", + "sha256:cd401ccbc2a249a47a3a1724c2918fcd04be1f7b54eb2a5a71ff915db0ac51c6", + "sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514", + "sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff", + "sha256:f8851ab9041756003119368c1e6cd0b9c631f46d686b3904b18c0139f4419f80" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.4.0" }, "python-dateutil": { "hashes": [ @@ -497,11 +507,11 @@ }, "python-dotenv": { "hashes": [ - "sha256:25c0ff1a3e12f4bde8d592cc254ab075cfe734fc5dd989036716fd17ee7e5ec7", - "sha256:3b9909bc96b0edc6b01586e1eed05e71174ef4e04c71da5786370cebea53ad74" + "sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d", + "sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423" ], "index": "pypi", - "version": "==0.13.0" + "version": "==0.14.0" }, "python-magic": { "hashes": [ @@ -527,6 +537,9 @@ "version": "==2020.1" }, "raven": { + "extras": [ + "flask" + ], "hashes": [ "sha256:3fa6de6efa2493a7c827472e984ce9b020797d0da16f1db67197bcc23c8fae54", "sha256:44a13f87670836e153951af9a3c80405d36b43097db869a36e92809673692ce4" @@ -536,37 +549,37 @@ }, "regex": { "hashes": [ - "sha256:1386e75c9d1574f6aa2e4eb5355374c8e55f9aac97e224a8a5a6abded0f9c927", - "sha256:27ff7325b297fb6e5ebb70d10437592433601c423f5acf86e5bc1ee2919b9561", - "sha256:329ba35d711e3428db6b45a53b1b13a0a8ba07cbbcf10bbed291a7da45f106c3", - "sha256:3a9394197664e35566242686d84dfd264c07b20f93514e2e09d3c2b3ffdf78fe", - "sha256:51f17abbe973c7673a61863516bdc9c0ef467407a940f39501e786a07406699c", - "sha256:579ea215c81d18da550b62ff97ee187b99f1b135fd894a13451e00986a080cad", - "sha256:70c14743320a68c5dac7fc5a0f685be63bc2024b062fe2aaccc4acc3d01b14a1", - "sha256:7e61be8a2900897803c293247ef87366d5df86bf701083b6c43119c7c6c99108", - "sha256:8044d1c085d49673aadb3d7dc20ef5cb5b030c7a4fa253a593dda2eab3059929", - "sha256:89d76ce33d3266173f5be80bd4efcbd5196cafc34100fdab814f9b228dee0fa4", - "sha256:99568f00f7bf820c620f01721485cad230f3fb28f57d8fbf4a7967ec2e446994", - "sha256:a7c37f048ec3920783abab99f8f4036561a174f1314302ccfa4e9ad31cb00eb4", - "sha256:c2062c7d470751b648f1cacc3f54460aebfc261285f14bc6da49c6943bd48bdd", - "sha256:c9bce6e006fbe771a02bda468ec40ffccbf954803b470a0345ad39c603402577", - "sha256:ce367d21f33e23a84fb83a641b3834dd7dd8e9318ad8ff677fbfae5915a239f7", - "sha256:ce450ffbfec93821ab1fea94779a8440e10cf63819be6e176eb1973a6017aff5", - "sha256:ce5cc53aa9fbbf6712e92c7cf268274eaff30f6bd12a0754e8133d85a8fb0f5f", - "sha256:d466967ac8e45244b9dfe302bbe5e3337f8dc4dec8d7d10f5e950d83b140d33a", - "sha256:d881c2e657c51d89f02ae4c21d9adbef76b8325fe4d5cf0e9ad62f850f3a98fd", - "sha256:e565569fc28e3ba3e475ec344d87ed3cd8ba2d575335359749298a0899fe122e", - "sha256:ea55b80eb0d1c3f1d8d784264a6764f931e172480a2f1868f2536444c5f01e01" - ], - "version": "==2020.5.14" + "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204", + "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162", + "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f", + "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb", + "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6", + "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7", + "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88", + "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99", + "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644", + "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a", + "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840", + "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067", + "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd", + "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4", + "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e", + "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89", + "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e", + "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc", + "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf", + "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341", + "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7" + ], + "version": "==2020.7.14" }, "requests": { "hashes": [ - "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", - "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" + "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", + "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" ], "index": "pypi", - "version": "==2.23.0" + "version": "==2.24.0" }, "requests-file": { "hashes": [ @@ -596,6 +609,7 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "soupsieve": { @@ -603,6 +617,7 @@ "sha256:1634eea42ab371d3d346309b93df7870a88610f0725d47528be902a0d95ecc55", "sha256:a59dc181727e95d25f781f0eb4fd1825ff45590ec8ff49eadfd7f1a537cc0232" ], + "markers": "python_version >= '3.5'", "version": "==2.0.1" }, "tabulate": { @@ -620,6 +635,14 @@ "index": "pypi", "version": "==2.2.2" }, + "toml": { + "hashes": [ + "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", + "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" + ], + "index": "pypi", + "version": "==0.10.1" + }, "tzlocal": { "hashes": [ "sha256:643c97c5294aedc737780a49d9df30889321cbe1204eac2c2ec6134035a92e44", @@ -629,62 +652,66 @@ }, "urllib3": { "hashes": [ - "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", - "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" + "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", + "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" ], - "version": "==1.25.9" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.25.10" }, "wcwidth": { "hashes": [ - "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1", - "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1" + "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784", + "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83" ], - "version": "==0.1.9" + "version": "==0.2.5" }, "werkzeug": { "hashes": [ "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43", "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==1.0.1" }, "wtforms": { "hashes": [ - "sha256:6ff8635f4caeed9f38641d48cfe019d0d3896f41910ab04494143fc027866e1b", - "sha256:861a13b3ae521d6700dac3b2771970bd354a63ba7043ecc3a82b5288596a1972" + "sha256:7b504fc724d0d1d4d5d5c114e778ec88c37ea53144683e084215eed5155ada4c", + "sha256:81195de0ac94fbc8368abbaf9197b88c4f3ffd6c2719b5bf5fc9da744f3d829c" ], "index": "pypi", - "version": "==2.3.1" + "version": "==2.3.3" } }, "develop": { "astroid": { "hashes": [ - "sha256:4c17cea3e592c21b6e222f673868961bad77e1f985cb1694ed077475a89229c1", - "sha256:d8506842a3faf734b81599c8b98dcc423de863adcc1999248480b18bd31a0f38" + "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703", + "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386" ], - "version": "==2.4.1" + "markers": "python_version >= '3.5'", + "version": "==2.4.2" }, "attrs": { "hashes": [ "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==19.3.0" }, "backcall": { "hashes": [ - "sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4", - "sha256:bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2" + "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e", + "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255" ], - "version": "==0.1.0" + "version": "==0.2.0" }, "certifi": { "hashes": [ - "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304", - "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519" + "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", + "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" ], - "version": "==2020.4.5.1" + "version": "==2020.6.20" }, "chardet": { "hashes": [ @@ -695,39 +722,43 @@ }, "coverage": { "hashes": [ - "sha256:00f1d23f4336efc3b311ed0d807feb45098fc86dee1ca13b3d6768cdab187c8a", - "sha256:01333e1bd22c59713ba8a79f088b3955946e293114479bbfc2e37d522be03355", - "sha256:0cb4be7e784dcdc050fc58ef05b71aa8e89b7e6636b99967fadbdba694cf2b65", - "sha256:0e61d9803d5851849c24f78227939c701ced6704f337cad0a91e0972c51c1ee7", - "sha256:1601e480b9b99697a570cea7ef749e88123c04b92d84cedaa01e117436b4a0a9", - "sha256:2742c7515b9eb368718cd091bad1a1b44135cc72468c731302b3d641895b83d1", - "sha256:2d27a3f742c98e5c6b461ee6ef7287400a1956c11421eb574d843d9ec1f772f0", - "sha256:402e1744733df483b93abbf209283898e9f0d67470707e3c7516d84f48524f55", - "sha256:5c542d1e62eece33c306d66fe0a5c4f7f7b3c08fecc46ead86d7916684b36d6c", - "sha256:5f2294dbf7875b991c381e3d5af2bcc3494d836affa52b809c91697449d0eda6", - "sha256:6402bd2fdedabbdb63a316308142597534ea8e1895f4e7d8bf7476c5e8751fef", - "sha256:66460ab1599d3cf894bb6baee8c684788819b71a5dc1e8fa2ecc152e5d752019", - "sha256:782caea581a6e9ff75eccda79287daefd1d2631cc09d642b6ee2d6da21fc0a4e", - "sha256:79a3cfd6346ce6c13145731d39db47b7a7b859c0272f02cdb89a3bdcbae233a0", - "sha256:7a5bdad4edec57b5fb8dae7d3ee58622d626fd3a0be0dfceda162a7035885ecf", - "sha256:8fa0cbc7ecad630e5b0f4f35b0f6ad419246b02bc750de7ac66db92667996d24", - "sha256:a027ef0492ede1e03a8054e3c37b8def89a1e3c471482e9f046906ba4f2aafd2", - "sha256:a3f3654d5734a3ece152636aad89f58afc9213c6520062db3978239db122f03c", - "sha256:a82b92b04a23d3c8a581fc049228bafde988abacba397d57ce95fe95e0338ab4", - "sha256:acf3763ed01af8410fc36afea23707d4ea58ba7e86a8ee915dfb9ceff9ef69d0", - "sha256:adeb4c5b608574a3d647011af36f7586811a2c1197c861aedb548dd2453b41cd", - "sha256:b83835506dfc185a319031cf853fa4bb1b3974b1f913f5bb1a0f3d98bdcded04", - "sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e", - "sha256:bf9cb9a9fd8891e7efd2d44deb24b86d647394b9705b744ff6f8261e6f29a730", - "sha256:c317eaf5ff46a34305b202e73404f55f7389ef834b8dbf4da09b9b9b37f76dd2", - "sha256:dbe8c6ae7534b5b024296464f387d57c13caa942f6d8e6e0346f27e509f0f768", - "sha256:de807ae933cfb7f0c7d9d981a053772452217df2bf38e7e6267c9cbf9545a796", - "sha256:dead2ddede4c7ba6cb3a721870f5141c97dc7d85a079edb4bd8d88c3ad5b20c7", - "sha256:dec5202bfe6f672d4511086e125db035a52b00f1648d6407cc8e526912c0353a", - "sha256:e1ea316102ea1e1770724db01998d1603ed921c54a86a2efcb03428d5417e489", - "sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052" - ], - "version": "==5.1" + "sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb", + "sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3", + "sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716", + "sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034", + "sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3", + "sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8", + "sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0", + "sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f", + "sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4", + "sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962", + "sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d", + "sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b", + "sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4", + "sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3", + "sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258", + "sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59", + "sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01", + "sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd", + "sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b", + "sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d", + "sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89", + "sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd", + "sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b", + "sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d", + "sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46", + "sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546", + "sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082", + "sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b", + "sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4", + "sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8", + "sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811", + "sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd", + "sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651", + "sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==5.2.1" }, "decorator": { "hashes": [ @@ -738,34 +769,35 @@ }, "flake8": { "hashes": [ - "sha256:c69ac1668e434d37a2d2880b3ca9aafd54b3a10a3ac1ab101d22f29e29cf8634", - "sha256:ccaa799ef9893cebe69fdfefed76865aeaefbb94cb8545617b2298786a4de9a5" + "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c", + "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208" ], "index": "pypi", - "version": "==3.8.2" + "version": "==3.8.3" }, "idna": { "hashes": [ - "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", - "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "version": "==2.9" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" }, "importlib-metadata": { "hashes": [ - "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f", - "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e" + "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83", + "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070" ], "markers": "python_version < '3.8'", - "version": "==1.6.0" + "version": "==1.7.0" }, "ipython": { "hashes": [ - "sha256:5b241b84bbf0eb085d43ae9d46adf38a13b45929ca7774a740990c2c242534bb", - "sha256:f0126781d0f959da852fb3089e170ed807388e986a8dd4e6ac44855845b0fb1c" + "sha256:2dbcc8c27ca7d3cfe4fcdff7f45b27f9a8d3edfa70ff8024a71c7a8eb5f09d64", + "sha256:9f4fcb31d3b2c533333893b9172264e4821c1ac91839500f31bd43f2c59b3ccf" ], "index": "pypi", - "version": "==7.14.0" + "version": "==7.16.1" }, "ipython-genutils": { "hashes": [ @@ -779,14 +811,16 @@ "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==4.3.21" }, "jedi": { "hashes": [ - "sha256:cd60c93b71944d628ccac47df9a60fec53150de53d42dc10a7fc4b5ba6aae798", - "sha256:df40c97641cb943661d2db4c33c2e1ff75d491189423249e989bcea4464f3030" + "sha256:86ed7d9b750603e4ba582ea8edc678657fb4007894a12bcf6f4bb97892f31d20", + "sha256:98cc583fa0f2f8304968199b01b6b4b94f469a1f4a74c1560506ca2a211378b5" ], - "version": "==0.17.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.17.2" }, "lazy-object-proxy": { "hashes": [ @@ -812,6 +846,7 @@ "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.4.3" }, "mccabe": { @@ -823,24 +858,27 @@ }, "more-itertools": { "hashes": [ - "sha256:558bb897a2232f5e4f8e2399089e35aecb746e1f9191b6584a151647e89267be", - "sha256:7818f596b1e87be009031c7653d01acc46ed422e6656b394b0f765ce66ed4982" + "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5", + "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2" ], - "version": "==8.3.0" + "markers": "python_version >= '3.5'", + "version": "==8.4.0" }, "packaging": { "hashes": [ "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.4" }, "parso": { "hashes": [ - "sha256:158c140fc04112dc45bca311633ae5033c2c2a7b732fa33d0955bad8152a8dd0", - "sha256:908e9fae2144a076d72ae4e25539143d40b8e3eafbaeae03c1bfe226f4cdf12c" + "sha256:97218d9159b2520ff45eb78028ba8b50d2bc61dcc062a9682666f2dc4bd331ea", + "sha256:caba44724b994a8a5e086460bb212abc5a8bc46951bf4a9a1210745953622eb9" ], - "version": "==0.7.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.7.1" }, "pexpect": { "hashes": [ @@ -869,6 +907,7 @@ "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.13.1" }, "prompt-toolkit": { @@ -876,6 +915,7 @@ "sha256:563d1a4140b63ff9dd587bda9557cffb2fe73650205ab6f4383092fb882e7dc8", "sha256:df7e9e63aea609b1da3a65641ceaf5bc7d05e0a04de5bd45d05dbeffbabf9e04" ], + "markers": "python_full_version >= '3.6.1'", "version": "==3.0.5" }, "psycopg2": { @@ -894,6 +934,7 @@ "sha256:d3b29d717d39d3580efd760a9a46a7418408acebbb784717c90d708c9ed5f055", "sha256:f7d46240f7a1ae1dd95aab38bd74f7428d46531f69219954266d669da60c0818" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.5" }, "ptyprocess": { @@ -905,16 +946,18 @@ }, "py": { "hashes": [ - "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", - "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0" + "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2", + "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342" ], - "version": "==1.8.1" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.9.0" }, "pycodestyle": { "hashes": [ "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.6.0" }, "pyflakes": { @@ -922,6 +965,7 @@ "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.2.0" }, "pygments": { @@ -929,46 +973,48 @@ "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44", "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324" ], + "markers": "python_version >= '3.5'", "version": "==2.6.1" }, "pylint": { "hashes": [ - "sha256:b95e31850f3af163c2283ed40432f053acbc8fc6eba6a069cb518d9dbf71848c", - "sha256:dd506acce0427e9e08fb87274bcaa953d38b50a58207170dbf5b36cf3e16957b" + "sha256:7dd78437f2d8d019717dbf287772d0b2dbdfd13fc016aa7faa08d67bccc46adc", + "sha256:d0ece7d223fe422088b0e8f13fa0a1e8eb745ebffcb8ed53d3e95394b6101a1c" ], "index": "pypi", - "version": "==2.5.2" + "version": "==2.5.3" }, "pyparsing": { "hashes": [ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.4.7" }, "pytest": { "hashes": [ - "sha256:95c710d0a72d91c13fae35dce195633c929c3792f54125919847fdcdf7caa0d3", - "sha256:eb2b5e935f6a019317e455b6da83dd8650ac9ffd2ee73a7b657a30873d67a698" + "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1", + "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8" ], "index": "pypi", - "version": "==5.4.2" + "version": "==5.4.3" }, "pytest-cov": { "hashes": [ - "sha256:b6a814b8ed6247bd81ff47f038511b57fe1ce7f4cc25b9106f1a4b106f1d9322", - "sha256:c87dfd8465d865655a8213859f1b4749b43448b5fae465cb981e16d52a811424" + "sha256:1a629dc9f48e53512fcbfda6b07de490c374b0c83c55ff7a1720b3fccff0ac87", + "sha256:6e6d18092dce6fad667cd7020deed816f858ad3b49d5b5e2b1cc1c97a4dba65c" ], "index": "pypi", - "version": "==2.9.0" + "version": "==2.10.0" }, "pytest-mock": { "hashes": [ - "sha256:997729451dfc36b851a9accf675488c7020beccda15e11c75632ee3d1b1ccd71", - "sha256:ce610831cedeff5331f4e2fc453a5dd65384303f680ab34bee2c6533855b431c" + "sha256:5564c7cd2569b603f8451ec77928083054d8896046830ca763ed68f4112d17c7", + "sha256:7122d55505d5ed5a6f3df940ad174b3f606ecae5e9bc379569cdcbd4cd9d2b83" ], "index": "pypi", - "version": "==3.1.0" + "version": "==3.2.0" }, "pytest-pylint": { "hashes": [ @@ -987,25 +1033,26 @@ }, "requests": { "hashes": [ - "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", - "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" + "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", + "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" ], "index": "pypi", - "version": "==2.23.0" + "version": "==2.24.0" }, "responses": { "hashes": [ - "sha256:1a78bc010b20a5022a2c0cb76b8ee6dc1e34d887972615ebd725ab9a166a4960", - "sha256:3d596d0be06151330cb230a2d630717ab20f7a81f205019481e206eb5db79915" + "sha256:7bb697a5fedeb41d81e8b87f152d453d5cab42dcd1691b6a7d6097e94d33f373", + "sha256:af94d28cdfb48ded0ad82a5216616631543650f440334a693479b8991a6594a2" ], "index": "pypi", - "version": "==0.10.14" + "version": "==0.10.15" }, "six": { "hashes": [ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "toml": { @@ -1013,6 +1060,7 @@ "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" ], + "index": "pypi", "version": "==0.10.1" }, "traitlets": { @@ -1046,22 +1094,23 @@ "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" ], - "markers": "implementation_name == 'cpython' and python_version < '3.8'", + "markers": "python_version < '3.8' and implementation_name == 'cpython'", "version": "==1.4.1" }, "urllib3": { "hashes": [ - "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", - "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" + "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", + "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" ], - "version": "==1.25.9" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.25.10" }, "wcwidth": { "hashes": [ - "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1", - "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1" + "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784", + "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83" ], - "version": "==0.1.9" + "version": "==0.2.5" }, "wrapt": { "hashes": [ @@ -1074,6 +1123,7 @@ "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" ], + "markers": "python_version >= '3.6'", "version": "==3.1.0" } } diff --git a/python/fatcat_tools/transforms/__init__.py b/python/fatcat_tools/transforms/__init__.py index 3f4700ff..7cc8f699 100644 --- a/python/fatcat_tools/transforms/__init__.py +++ b/python/fatcat_tools/transforms/__init__.py @@ -1,5 +1,5 @@ -from .entities import entity_to_dict, entity_from_json, entity_from_dict +from .entities import entity_to_dict, entity_from_json, entity_from_dict, entity_from_toml, entity_to_toml from .elasticsearch import release_to_elasticsearch, container_to_elasticsearch, changelog_to_elasticsearch, file_to_elasticsearch from .csl import release_to_csl, citeproc_csl from .ingest import release_ingest_request diff --git a/python/fatcat_tools/transforms/entities.py b/python/fatcat_tools/transforms/entities.py index 53455e85..3892d54a 100644 --- a/python/fatcat_tools/transforms/entities.py +++ b/python/fatcat_tools/transforms/entities.py @@ -1,9 +1,10 @@ import json +import toml import collections from fatcat_openapi_client import ApiClient -def entity_to_dict(entity, api_client=None): +def entity_to_dict(entity, api_client=None) -> dict: """ Hack to take advantage of the code-generated serialization code. @@ -17,7 +18,7 @@ def entity_to_dict(entity, api_client=None): api_client = ApiClient() return api_client.sanitize_for_serialization(entity) -def entity_from_json(json_str, entity_type, api_client=None): +def entity_from_json(json_str: str, entity_type, api_client=None): """ Hack to take advantage of the code-generated deserialization code @@ -29,6 +30,21 @@ def entity_from_json(json_str, entity_type, api_client=None): thing.data = json_str return api_client.deserialize(thing, entity_type) -def entity_from_dict(obj, entity_type, api_client=None): +def entity_from_dict(obj: dict, entity_type, api_client=None): json_str = json.dumps(obj) return entity_from_json(json_str, entity_type, api_client=api_client) + +def entity_to_toml(entity, api_client=None, pop_fields=None) -> str: + """ + pop_fields parameter can be used to strip out some fields from the resulting + TOML. Eg, for fields which should not be edited, like the ident. + """ + obj = entity_to_dict(entity, api_client=api_client) + pop_fields = pop_fields or [] + for k in pop_fields: + obj.pop(k, None) + return toml.dumps(obj) + +def entity_from_toml(toml_str: str, entity_type, api_client=None): + obj = toml.loads(toml_str) + return entity_from_dict(obj, entity_type, api_client=api_client) diff --git a/python/fatcat_web/editing_routes.py b/python/fatcat_web/editing_routes.py index ffce35f3..8e3b03b0 100644 --- a/python/fatcat_web/editing_routes.py +++ b/python/fatcat_web/editing_routes.py @@ -1,8 +1,10 @@ +from typing import Optional + from flask import render_template, abort, redirect, session, flash from flask_login import login_required -from fatcat_openapi_client import Editgroup +from fatcat_openapi_client import * from fatcat_openapi_client.rest import ApiException from fatcat_tools.transforms import * from fatcat_web import app, api, auth_api @@ -13,6 +15,104 @@ from fatcat_web.entity_helpers import * ### Helper Methods ########################################################## +def generic_entity_create_from_toml(user_api, entity_type: str, editgroup_id: str, toml_str: str) -> EntityEdit: + if entity_type == 'container': + entity = entity_from_toml(toml_str, ContainerEntity) + edit = user_api.create_container(editgroup_id, entity) + elif entity_type == 'creator': + entity = entity_from_toml(toml_str, CreatorEntity) + edit = user_api.create_creator(editgroup_id, entity) + elif entity_type == 'file': + entity = entity_from_toml(toml_str, FileEntity) + edit = user_api.create_file(editgroup_id, entity) + elif entity_type == 'fileset': + entity = entity_from_toml(toml_str, FilesetEntity) + edit = user_api.create_fileset(editgroup_id, entity) + elif entity_type == 'webcapture': + entity = entity_from_toml(toml_str, WebcaptureEntity) + edit = user_api.create_webcapture(editgroup_id, entity) + elif entity_type == 'release': + entity = entity_from_toml(toml_str, ReleaseEntity) + edit = user_api.create_release(editgroup_id, entity) + elif entity_type == 'work': + entity = entity_from_toml(toml_str, WorkEntity) + edit = user_api.create_work(editgroup_id, entity) + else: + raise NotImplementedError + return edit + +def generic_entity_delete_edit(user_api, entity_type: str, editgroup_id: str, edit_id: str) -> None: + try: + if entity_type == 'container': + user_api.delete_container_edit(editgroup_id, edit_id) + elif entity_type == 'creator': + user_api.delete_creator_edit(editgroup_id, edit_id) + elif entity_type == 'file': + user_api.delete_file_edit(editgroup_id, edit_id) + elif entity_type == 'fileset': + user_api.delete_fileset_edit(editgroup_id, edit_id) + elif entity_type == 'webcapture': + user_api.delete_webcapture_edit(editgroup_id, edit_id) + elif entity_type == 'release': + user_api.delete_release_edit(editgroup_id, edit_id) + elif entity_type == 'work': + user_api.delete_work_edit(editgroup_id, edit_id) + else: + raise NotImplementedError + except ApiException as ae: + if ae.status == 404: + pass + else: + raise ae + +def generic_entity_delete_entity(user_api, entity_type: str, editgroup_id: str, entity_ident: str) -> None: + try: + if entity_type == 'container': + edit = user_api.delete_container(editgroup_id, entity_ident) + elif entity_type == 'creator': + edit = user_api.delete_creator(editgroup_id, entity_ident) + elif entity_type == 'file': + edit = user_api.delete_file(editgroup_id, entity_ident) + elif entity_type == 'fileset': + edit = user_api.delete_fileset(editgroup_id, entity_ident) + elif entity_type == 'webcapture': + edit = user_api.delete_webcapture(editgroup_id, entity_ident) + elif entity_type == 'release': + edit = user_api.delete_release(editgroup_id, entity_ident) + elif entity_type == 'work': + edit = user_api.delete_work(editgroup_id, entity_ident) + else: + raise NotImplementedError + except ApiException as ae: + raise ae + return edit + +def generic_entity_update_from_toml(user_api, entity_type: str, editgroup_id: str, existing_ident, toml_str: str) -> EntityEdit: + if entity_type == 'container': + entity = entity_from_toml(toml_str, ContainerEntity) + edit = user_api.update_container(editgroup_id, existing_ident, entity) + elif entity_type == 'creator': + entity = entity_from_toml(toml_str, CreatorEntity) + edit = user_api.update_creator(editgroup_id, existing_ident, entity) + elif entity_type == 'file': + entity = entity_from_toml(toml_str, FileEntity) + edit = user_api.update_file(editgroup_id, existing_ident, entity) + elif entity_type == 'fileset': + entity = entity_from_toml(toml_str, FilesetEntity) + edit = user_api.update_fileset(editgroup_id, existing_ident, entity) + elif entity_type == 'webcapture': + entity = entity_from_toml(toml_str, WebcaptureEntity) + edit = user_api.update_webcapture(editgroup_id, existing_ident, entity) + elif entity_type == 'release': + entity = entity_from_toml(toml_str, ReleaseEntity) + edit = user_api.update_release(editgroup_id, existing_ident, entity) + elif entity_type == 'work': + entity = entity_from_toml(toml_str, WorkEntity) + edit = user_api.update_work(editgroup_id, existing_ident, entity) + else: + raise NotImplementedError + return edit + def form_editgroup_get_or_create(api, edit_form): """ This function expects a submitted, validated edit form @@ -75,7 +175,7 @@ def generic_entity_edit(editgroup_id, entity_type, existing_ident, edit_template try: editgroup = api.get_editgroup(editgroup_id) except ApiException as ae: - abort(ae.status) + raise ae # check that editgroup is edit-able if editgroup.changelog_index != None: @@ -138,14 +238,7 @@ def generic_entity_edit(editgroup_id, entity_type, existing_ident, edit_template # a "update pointer" edit existing.revision = None try: - if entity_type == 'container': - user_api.delete_container_edit(editgroup.editgroup_id, existing_edit.edit_id) - elif entity_type == 'file': - user_api.delete_file_edit(editgroup.editgroup_id, existing_edit.edit_id) - elif entity_type == 'release': - user_api.delete_release_edit(editgroup.editgroup_id, existing_edit.edit_id) - else: - raise NotImplementedError + generic_entity_delete_edit(user_api, entity_type, editgroup.editgroup_id, existing_edit.edit_id) except ApiException as ae: if ae.status == 404: pass @@ -194,7 +287,13 @@ def generic_entity_edit(editgroup_id, entity_type, existing_ident, edit_template existing_ident=existing_ident, editgroup=editgroup, potential_editgroups=potential_editgroups), status -def generic_edit_delete(editgroup_id, entity_type, edit_id): +def generic_entity_toml_edit(editgroup_id, entity_type, existing_ident, edit_template): + """ + Similar to generic_entity_edit(), but for TOML editing mode. + + Handles both creation and update/edit paths. + """ + # fetch editgroup (if set) or 404 editgroup = None if editgroup_id: @@ -205,23 +304,185 @@ def generic_edit_delete(editgroup_id, entity_type, edit_id): # check that editgroup is edit-able if editgroup.changelog_index != None: - abort(400, "Editgroup already merged") + flash("Editgroup already merged") + abort(400) + + # fetch entity (if set) or 404 + existing = None + existing_edit = None + if editgroup and existing_ident: + existing, existing_edit = generic_get_editgroup_entity(editgroup, entity_type, existing_ident) + elif existing_ident: + existing = generic_get_entity(entity_type, existing_ident) + + # parse form (if submitted) + status = 200 + form = EntityTomlForm() + + if form.is_submitted(): + if form.validate_on_submit(): + # API on behalf of user + user_api = auth_api(session['api_token']) + if not editgroup: + editgroup = form_editgroup_get_or_create(user_api, form) + + if editgroup: + + if not existing_ident: # it's a create + try: + edit = generic_entity_create_from_toml(user_api, entity_type, editgroup.editgroup_id, form.toml.data) + except ValueError as ve: + form.toml.errors = [ve] + status = 400 + except ApiException as ae: + app.log.warning(ae) + raise ae + if status == 200: + return redirect('/editgroup/{}/{}/{}'.format(editgroup.editgroup_id, entity_type, edit.ident)) + else: # it's an update + # TODO: some danger of wiping database state here is + # "updated edit" causes, eg, a 4xx error. Better to allow + # this in the API itself. For now, form validation *should* + # catch most errors, and if not editor can hit back and try + # again. This means, need to allow failure of deletion. + if existing_edit: + # need to clear revision on object or this becomes just + # a "update pointer" edit + existing.revision = None + generic_entity_delete_edit(user_api, entity_type, editgroup.editgroup_id, existing_edit.edit_id) + try: + edit = generic_entity_update_from_toml(user_api, entity_type, editgroup.editgroup_id, existing.ident, form.toml.data) + except ValueError as ve: + form.toml.errors = [ve] + status = 400 + except ApiException as ae: + app.log.warning(ae) + raise ae + if status == 200: + return redirect('/editgroup/{}/{}/{}'.format(editgroup.editgroup_id, entity_type, edit.ident)) + else: + status = 400 + elif form.errors: + status = 400 + app.log.info("form errors (did not validate): {}".format(form.errors)) + + else: # form is not submitted + if existing: + form = EntityTomlForm.from_entity(existing) + + editor_editgroups = api.get_editor_editgroups(session['editor']['editor_id'], limit=20) + potential_editgroups = [e for e in editor_editgroups if e.changelog_index == None and e.submitted == None] + + if not form.is_submitted(): + # default to most recent not submitted, fallback to "create new" + form.editgroup_id.data = "" + if potential_editgroups: + form.editgroup_id.data = potential_editgroups[0].editgroup_id + + return render_template(edit_template, form=form, entity_type=entity_type, + existing_ident=existing_ident, editgroup=editgroup, + potential_editgroups=potential_editgroups), status + +def generic_entity_delete(editgroup_id: Optional[str], entity_type: str, existing_ident: str): + """ + Similar to generic_entity_edit(), but for deleting entities. This is a bit + simpler! + + Handles both creation and update/edit paths. + """ + + # fetch editgroup (if set) or 404 + editgroup = None + if editgroup_id: + try: + editgroup = api.get_editgroup(editgroup_id) + except ApiException as ae: + raise ae + + # check that editgroup is edit-able + if editgroup.changelog_index != None: + flash("Editgroup already merged") + abort(400) + + # fetch entity (if set) or 404 + existing = None + existing_edit = None + if editgroup and existing_ident: + existing, existing_edit = generic_get_editgroup_entity(editgroup, entity_type, existing_ident) + elif existing_ident: + existing = generic_get_entity(entity_type, existing_ident) + + # parse form (if submitted) + status = 200 + form = EntityEditForm() + + if form.is_submitted(): + if form.validate_on_submit(): + # API on behalf of user + user_api = auth_api(session['api_token']) + if not editgroup: + editgroup = form_editgroup_get_or_create(user_api, form) + + if editgroup: + # TODO: some danger of wiping database state here is + # "updated edit" causes, eg, a 4xx error. Better to allow + # this in the API itself. For now, form validation *should* + # catch most errors, and if not editor can hit back and try + # again. This means, need to allow failure of deletion. + if existing_edit: + # need to clear revision on object or this becomes just + # a "update pointer" edit + existing.revision = None + generic_entity_delete_edit(user_api, entity_type, editgroup.editgroup_id, existing_edit.edit_id) + try: + edit = generic_entity_delete_entity(user_api, entity_type, editgroup.editgroup_id, existing.ident) + except ApiException as ae: + app.log.warning(ae) + raise ae + if status == 200: + return redirect('/editgroup/{}/{}/{}'.format(editgroup.editgroup_id, entity_type, edit.ident)) + else: + status = 400 + elif form.errors: + status = 400 + app.log.info("form errors (did not validate): {}".format(form.errors)) + + else: # form is not submitted + if existing: + form = EntityTomlForm.from_entity(existing) + + editor_editgroups = api.get_editor_editgroups(session['editor']['editor_id'], limit=20) + potential_editgroups = [e for e in editor_editgroups if e.changelog_index == None and e.submitted == None] + + if not form.is_submitted(): + # default to most recent not submitted, fallback to "create new" + form.editgroup_id.data = "" + if potential_editgroups: + form.editgroup_id.data = potential_editgroups[0].editgroup_id + + return render_template("entity_delete.html", form=form, entity_type=entity_type, + existing_ident=existing_ident, editgroup=editgroup, + potential_editgroups=potential_editgroups), status + +def generic_edit_delete(editgroup_id, entity_type, edit_id): + # fetch editgroup (if set) or 404 + editgroup = None + if editgroup_id: + try: + editgroup = api.get_editgroup(editgroup_id) + except ApiException as ae: + abort(ae.status) + + # check that editgroup is edit-able + if editgroup.changelog_index != None: + flash("Editgroup already merged") + abort(400) # API on behalf of user user_api = auth_api(session['api_token']) # do the deletion - try: - if entity_type == 'container': - user_api.delete_container_edit(editgroup.editgroup_id, edit_id) - elif entity_type == 'file': - user_api.delete_file_edit(editgroup.editgroup_id, edit_id) - elif entity_type == 'release': - user_api.delete_release_edit(editgroup.editgroup_id, edit_id) - else: - raise NotImplementedError - except ApiException as ae: - raise ae + generic_entity_delete_edit(user_api, entity_type, editgroup.editgroup_id, edit_id) return redirect("/editgroup/{}".format(editgroup_id)) @@ -234,19 +495,43 @@ def container_create_view(): @app.route('/container/<ident>/edit', methods=['GET', 'POST']) @login_required -def container_edit(ident): +def container_edit_view(ident): return generic_entity_edit(None, 'container', ident, 'container_edit.html') +@app.route('/container/<ident>/delete', methods=['GET', 'POST']) +@login_required +def container_delete_view(ident): + return generic_entity_delete(None, 'container', ident) + @app.route('/editgroup/<editgroup_id>/container/<ident>/edit', methods=['GET', 'POST']) @login_required -def container_editgroup_edit(editgroup_id, ident): +def container_editgroup_edit_view(editgroup_id, ident): return generic_entity_edit(editgroup_id, 'container', ident, 'container_edit.html') +@app.route('/editgroup/<editgroup_id>/container/<ident>/delete', methods=['GET', 'POST']) +@login_required +def container_editgroup_delete_view(editgroup_id, ident): + return generic_entity_delete(editgroup_id, 'container', ident) + @app.route('/editgroup/<editgroup_id>/container/edit/<edit_id>/delete', methods=['POST']) @login_required def container_edit_delete(editgroup_id, edit_id): return generic_edit_delete(editgroup_id, 'container', edit_id) +@app.route('/creator/<ident>/delete', methods=['GET', 'POST']) +@login_required +def creator_delete_view(ident): + return generic_entity_delete(None, 'creator', ident) + +@app.route('/editgroup/<editgroup_id>/creator/edit/<edit_id>/delete', methods=['POST']) +def creator_edit_delete(editgroup_id, edit_id): + return generic_edit_delete(editgroup_id, 'creator', edit_id) + +@app.route('/editgroup/<editgroup_id>/creator/<ident>/delete', methods=['GET', 'POST']) +@login_required +def creator_editgroup_delete(editgroup_id, ident): + return generic_entity_delete(editgroup_id, 'creator', ident) + @app.route('/file/create', methods=['GET', 'POST']) @login_required def file_create_view(): @@ -254,19 +539,57 @@ def file_create_view(): @app.route('/file/<ident>/edit', methods=['GET', 'POST']) @login_required -def file_edit(ident): +def file_edit_view(ident): return generic_entity_edit(None, 'file', ident, 'file_edit.html') +@app.route('/file/<ident>/delete', methods=['GET', 'POST']) +@login_required +def file_delete_view(ident): + return generic_entity_delete(None, 'file', ident) + @app.route('/editgroup/<editgroup_id>/file/<ident>/edit', methods=['GET', 'POST']) @login_required -def file_editgroup_edit(editgroup_id, ident): +def file_editgroup_edit_view(editgroup_id, ident): return generic_entity_edit(editgroup_id, 'file', ident, 'file_edit.html') +@app.route('/editgroup/<editgroup_id>/file/<ident>/delete', methods=['GET', 'POST']) +@login_required +def file_editgroup_delete_view(editgroup_id, ident): + return generic_entity_delete(editgroup_id, 'file', ident) + @app.route('/editgroup/<editgroup_id>/file/edit/<edit_id>/delete', methods=['POST']) @login_required def file_edit_delete(editgroup_id, edit_id): return generic_edit_delete(editgroup_id, 'file', edit_id) +@app.route('/fileset/<ident>/delete', methods=['GET', 'POST']) +@login_required +def fileset_delete_view(ident): + return generic_entity_delete(None, 'fileset', ident) + +@app.route('/editgroup/<editgroup_id>/fileset/edit/<edit_id>/delete', methods=['POST']) +def fileset_edit_delete(editgroup_id, edit_id): + return generic_edit_delete(editgroup_id, 'fileset', edit_id) + +@app.route('/editgroup/<editgroup_id>/fileset/<ident>/delete', methods=['GET', 'POST']) +@login_required +def fileset_editgroup_delete(editgroup_id, ident): + return generic_entity_delete(editgroup_id, 'fileset', ident) + +@app.route('/webcapture/<ident>/delete', methods=['GET', 'POST']) +@login_required +def webcapture_delete_view(ident): + return generic_entity_delete(None, 'webcapture', ident) + +@app.route('/editgroup/<editgroup_id>/webcapture/edit/<edit_id>/delete', methods=['POST']) +def webcapture_edit_delete(editgroup_id, edit_id): + return generic_edit_delete(editgroup_id, 'webcapture', edit_id) + +@app.route('/editgroup/<editgroup_id>/webcapture/<ident>/delete', methods=['GET', 'POST']) +@login_required +def webcapture_editgroup_delete(editgroup_id, ident): + return generic_entity_delete(editgroup_id, 'webcapture', ident) + @app.route('/release/create', methods=['GET', 'POST']) @login_required def release_create_view(): @@ -274,82 +597,208 @@ def release_create_view(): @app.route('/release/<ident>/edit', methods=['GET', 'POST']) @login_required -def release_edit(ident): +def release_edit_view(ident): return generic_entity_edit(None, 'release', ident, 'release_edit.html') +@app.route('/release/<ident>/delete', methods=['GET', 'POST']) +@login_required +def release_delete_view(ident): + return generic_entity_delete(None, 'release', ident) + @app.route('/editgroup/<editgroup_id>/release/<ident>/edit', methods=['GET', 'POST']) @login_required def release_editgroup_edit(editgroup_id, ident): return generic_entity_edit(editgroup_id, 'release', ident, 'release_edit.html') +@app.route('/editgroup/<editgroup_id>/release/<ident>/delete', methods=['GET', 'POST']) +@login_required +def release_editgroup_delete(editgroup_id, ident): + return generic_entity_delete(editgroup_id, 'release', ident) + @app.route('/editgroup/<editgroup_id>/release/edit/<edit_id>/delete', methods=['POST']) @login_required def release_edit_delete(editgroup_id, edit_id): return generic_edit_delete(editgroup_id, 'release', edit_id) +@app.route('/work/<ident>/delete', methods=['GET', 'POST']) +@login_required +def work_delete_view(ident): + return generic_entity_delete(None, 'work', ident) -### Not-Implemented Views ################################################### +@app.route('/editgroup/<editgroup_id>/work/edit/<edit_id>/delete', methods=['POST']) +def work_edit_delete(editgroup_id, edit_id): + return generic_edit_delete(editgroup_id, 'work', edit_id) + +@app.route('/editgroup/<editgroup_id>/work/<ident>/delete', methods=['GET', 'POST']) +@login_required +def work_editgroup_delete(editgroup_id, ident): + return generic_entity_delete(editgroup_id, 'work', ident) + +### TOML Views ############################################################## + +@app.route('/container/create/toml', methods=['GET', 'POST']) +@login_required +def container_create_toml_view(): + return generic_entity_toml_edit(None, 'container', None, 'entity_create_toml.html') + +@app.route('/container/<ident>/edit/toml', methods=['GET', 'POST']) +@login_required +def container_edit_toml_view(ident): + return generic_entity_toml_edit(None, 'container', ident, 'entity_edit_toml.html') + +@app.route('/editgroup/<editgroup_id>/container/<ident>/edit/toml', methods=['GET', 'POST']) +@login_required +def container_editgroup_edit_toml(editgroup_id, ident): + return generic_entity_toml_edit(editgroup_id, 'container', ident, 'entity_edit_toml.html') + +@app.route('/creator/create/toml', methods=['GET', 'POST']) +@login_required +def creator_create_toml_view(): + return generic_entity_toml_edit(None, 'creator', None, 'entity_create_toml.html') + +@app.route('/creator/<ident>/edit/toml', methods=['GET', 'POST']) +@login_required +def creator_edit_toml_view(ident): + return generic_entity_toml_edit(None, 'creator', ident, 'entity_edit_toml.html') + +@app.route('/editgroup/<editgroup_id>/creator/<ident>/edit/toml', methods=['GET', 'POST']) +@login_required +def creator_editgroup_edit_toml(editgroup_id, ident): + return generic_entity_toml_edit(editgroup_id, 'creator', ident, 'entity_edit_toml.html') + +@app.route('/file/create/toml', methods=['GET', 'POST']) +@login_required +def file_create_toml_view(): + return generic_entity_toml_edit(None, 'file', None, 'entity_create_toml.html') + +@app.route('/file/<ident>/edit/toml', methods=['GET', 'POST']) +@login_required +def file_edit_toml_view(ident): + return generic_entity_toml_edit(None, 'file', ident, 'entity_edit_toml.html') + +@app.route('/editgroup/<editgroup_id>/file/<ident>/edit/toml', methods=['GET', 'POST']) +@login_required +def file_editgroup_edit_toml(editgroup_id, ident): + return generic_entity_toml_edit(editgroup_id, 'file', ident, 'entity_edit_toml.html') + +@app.route('/fileset/create/toml', methods=['GET', 'POST']) +@login_required +def fileset_create_toml_view(): + return generic_entity_toml_edit(None, 'fileset', None, 'entity_create_toml.html') + +@app.route('/fileset/<ident>/edit/toml', methods=['GET', 'POST']) +@login_required +def fileset_edit_toml_view(ident): + return generic_entity_toml_edit(None, 'fileset', ident, 'entity_edit_toml.html') + +@app.route('/editgroup/<editgroup_id>/fileset/<ident>/edit/toml', methods=['GET', 'POST']) +@login_required +def fileset_editgroup_edit_toml(editgroup_id, ident): + return generic_entity_toml_edit(editgroup_id, 'fileset', ident, 'entity_edit_toml.html') + +@app.route('/webcapture/create/toml', methods=['GET', 'POST']) +@login_required +def webcapture_create_toml_view(): + return generic_entity_toml_edit(None, 'webcapture', None, 'entity_create_toml.html') + +@app.route('/webcapture/<ident>/edit/toml', methods=['GET', 'POST']) +@login_required +def webcapture_edit_toml_view(ident): + return generic_entity_toml_edit(None, 'webcapture', ident, 'entity_edit_toml.html') + +@app.route('/editgroup/<editgroup_id>/webcapture/<ident>/edit/toml', methods=['GET', 'POST']) +@login_required +def webcapture_editgroup_edit_toml(editgroup_id, ident): + return generic_entity_toml_edit(editgroup_id, 'webcapture', ident, 'entity_edit_toml.html') + +@app.route('/release/create/toml', methods=['GET', 'POST']) +@login_required +def release_create_toml_view(): + return generic_entity_toml_edit(None, 'release', None, 'entity_create_toml.html') + +@app.route('/release/<ident>/edit/toml', methods=['GET', 'POST']) +@login_required +def release_edit_toml_view(ident): + return generic_entity_toml_edit(None, 'release', ident, 'entity_edit_toml.html') + +@app.route('/editgroup/<editgroup_id>/release/<ident>/edit/toml', methods=['GET', 'POST']) +@login_required +def release_editgroup_edit_toml(editgroup_id, ident): + return generic_entity_toml_edit(editgroup_id, 'release', ident, 'entity_edit_toml.html') + +@app.route('/work/create/toml', methods=['GET', 'POST']) +@login_required +def work_create_toml_view(): + return generic_entity_toml_edit(None, 'work', None, 'entity_create_toml.html') + +@app.route('/work/<ident>/edit/toml', methods=['GET', 'POST']) +@login_required +def work_edit_toml_view(ident): + return generic_entity_toml_edit(None, 'work', ident, 'entity_edit_toml.html') + +@app.route('/editgroup/<editgroup_id>/work/<ident>/edit/toml', methods=['GET', 'POST']) +@login_required +def work_editgroup_edit_toml(editgroup_id, ident): + return generic_entity_toml_edit(editgroup_id, 'work', ident, 'entity_edit_toml.html') + +### TOML-Only Editing Redirects ################################################ @app.route('/creator/create', methods=['GET']) +@login_required def creator_create_view(): - return abort(404) + return redirect('/creator/create/toml') @app.route('/creator/<ident>/edit', methods=['GET']) -def creator_edit(ident): - return render_template('entity_edit.html'), 404 +@login_required +def creator_edit_view(ident): + return redirect(f'/creator/{ident}/edit/toml') @app.route('/editgroup/<editgroup_id>/creator/<ident>/edit', methods=['GET', 'POST']) +@login_required def creator_editgroup_edit(editgroup_id, ident): - return abort(404) - -@app.route('/editgroup/<editgroup_id>/creator/edit/<edit_id>/delete', methods=['POST']) -def creator_edit_delete(editgroup_id, edit_id): - return abort(404) + return redirect(f'/editgroup/{editgroup_id}/creator/{ident}/edit/toml') @app.route('/fileset/create', methods=['GET']) +@login_required def fileset_create_view(): - return abort(404) + return redirect('/fileset/create/toml') @app.route('/fileset/<ident>/edit', methods=['GET']) -def fileset_edit(ident): - return render_template('entity_edit.html'), 404 +@login_required +def fileset_edit_view(ident): + return redirect(f'/fileset/{ident}/edit/toml') @app.route('/editgroup/<editgroup_id>/fileset/<ident>/edit', methods=['GET', 'POST']) +@login_required def fileset_editgroup_edit(editgroup_id, ident): - return abort(404) - -@app.route('/editgroup/<editgroup_id>/fileset/edit/<edit_id>/delete', methods=['POST']) -def fileset_edit_delete(editgroup_id, edit_id): - return abort(404) + return redirect(f'/editgroup/{editgroup_id}/fileset/{ident}/edit/toml') @app.route('/webcapture/create', methods=['GET']) +@login_required def webcapture_create_view(): - return abort(404) + return redirect('/webcapture/create/toml') @app.route('/webcapture/<ident>/edit', methods=['GET']) -def webcapture_edit(ident): - return render_template('entity_edit.html'), 404 +@login_required +def webcapture_edit_view(ident): + return redirect(f'/webcapture/{ident}/edit/toml') @app.route('/editgroup/<editgroup_id>/webcapture/<ident>/edit', methods=['GET', 'POST']) +@login_required def webcapture_editgroup_edit(editgroup_id, ident): - return abort(404) - -@app.route('/editgroup/<editgroup_id>/webcapture/edit/<edit_id>/delete', methods=['POST']) -def webcapture_edit_delete(editgroup_id, edit_id): - return abort(404) + return redirect(f'/editgroup/{editgroup_id}/webcapture/{ident}/edit/toml') @app.route('/work/create', methods=['GET']) +@login_required def work_create_view(): - return abort(404) + return redirect('/work/create/toml') @app.route('/work/<ident>/edit', methods=['GET']) -def work_edit(ident): - return render_template('entity_edit.html'), 404 +@login_required +def work_edit_view(ident): + return redirect(f'/work/{ident}/edit/toml') @app.route('/editgroup/<editgroup_id>/work/<ident>/edit', methods=['GET', 'POST']) +@login_required def work_editgroup_edit(editgroup_id, ident): - return abort(404) - -@app.route('/editgroup/<editgroup_id>/work/edit/<edit_id>/delete', methods=['POST']) -def work_edit_delete(editgroup_id, edit_id): - return abort(404) + return redirect(f'/editgroup/{editgroup_id}/work/{ident}/edit/toml') diff --git a/python/fatcat_web/entity_helpers.py b/python/fatcat_web/entity_helpers.py index c9a57290..7156b9be 100644 --- a/python/fatcat_web/entity_helpers.py +++ b/python/fatcat_web/entity_helpers.py @@ -1,5 +1,6 @@ from flask import abort +from fatcat_openapi_client import * from fatcat_openapi_client.rest import ApiException, ApiValueError from fatcat_tools.transforms import * from fatcat_web import api @@ -148,6 +149,26 @@ def generic_get_entity_revision(entity_type, revision_id): except ApiValueError: abort(400) +def generic_deleted_entity(entity_type, ident): + if entity_type == 'container': + entity = ContainerEntity() + elif entity_type == 'creator': + entity = CreatorEntity() + elif entity_type == 'file': + entity = FileEntity() + elif entity_type == 'fileset': + entity = FilesetEntity() + elif entity_type == 'webcapture': + entity = WebcaptureEntity() + elif entity_type == 'release': + entity = ReleaseEntity() + elif entity_type == 'work': + entity = WorkEntity() + else: + raise NotImplementedError + entity.ident = ident + return entity + def generic_get_editgroup_entity(editgroup, entity_type, ident): if entity_type == 'container': edits = editgroup.edits.containers @@ -166,14 +187,18 @@ def generic_get_editgroup_entity(editgroup, entity_type, ident): else: raise NotImplementedError revision_id = None + edit = None for e in edits: if e.ident == ident: revision_id = e.revision edit = e break - if not revision_id: + if not edit: # couldn't find relevant edit in this editgroup abort(404) + if not revision_id: + # deletion, presumably + return generic_deleted_entity(entity_type, ident), edit try: entity = generic_get_entity_revision(entity_type, revision_id) diff --git a/python/fatcat_web/forms.py b/python/fatcat_web/forms.py index 15585bf6..ff885fcb 100644 --- a/python/fatcat_web/forms.py +++ b/python/fatcat_web/forms.py @@ -4,10 +4,14 @@ Note: in thoery could use, eg, https://github.com/christabor/swagger_wtforms, but can't find one that is actually maintained. """ +import datetime + +import toml from flask_wtf import FlaskForm from wtforms import SelectField, DateField, StringField, IntegerField, \ - HiddenField, FormField, FieldList, validators + HiddenField, FormField, FieldList, validators, ValidationError, TextAreaField +from fatcat_tools import entity_to_toml from fatcat_openapi_client import ContainerEntity, FileEntity, \ ReleaseEntity, ReleaseContrib, FileUrl, ReleaseExtIds @@ -29,6 +33,16 @@ release_stage_options = [ ('published', 'Published'), ('updated', 'Updated'), ] +withdrawn_status_options = [ + ('', 'Not Withdrawn (blank)'), + ('retracted', 'Retracted'), + ('withdrawn', 'Withdrawn'), + ('concern', 'Concern Noted'), + ('spam', 'Spam'), + ('legal', 'Legal Taketown'), + ('safety', 'Public Safety'), + ('national-security', 'National Security'), +] role_type_options = [ ('author', 'Author'), ('editor', 'Editor'), @@ -62,11 +76,25 @@ class ReleaseContribForm(FlaskForm): default='author') RELEASE_SIMPLE_ATTRS = ['title', 'original_title', 'work_id', 'container_id', - 'release_type', 'release_stage', 'release_date', 'volume', 'issue', - 'pages', 'publisher', 'language', 'license_slug'] + 'release_type', 'release_stage', 'withdrawn_status', 'release_date', + 'release_year', 'volume', 'issue', 'pages', 'publisher', 'language', + 'license_slug'] RELEASE_EXTID_ATTRS = ['doi', 'wikidata_qid', 'isbn13', 'pmid', 'pmcid'] +def valid_year(form, field): + if field.data > datetime.date.today().year + 5: + raise ValidationError( + f"Year is too far in the future: {field.data}") + if field.data < 10: + raise ValidationError( + f"Year is too far in the past: {field.data}") + +def valid_2char_ascii(form, field): + if len(field.data) != 2 or len(field.data.encode('utf-8')) != 2 or not field.data.isalpha() or field.data != field.data.lower(): + raise ValidationError( + f"Must be 2-character ISO format, lower case: {field.data}") + class ReleaseEntityForm(EntityEditForm): """ TODO: @@ -75,7 +103,7 @@ class ReleaseEntityForm(EntityEditForm): """ title = StringField('Title', [validators.DataRequired()]) - original_title = StringField('Original Title') + original_title = StringField('Title in Original Language (if different)') work_id = StringField('Work FCID', [validators.Optional(True), validators.Length(min=26, max=26)]) @@ -87,9 +115,14 @@ class ReleaseEntityForm(EntityEditForm): choices=release_type_options, default='') release_stage = SelectField(choices=release_stage_options) + withdrawn_status = SelectField("Withdrawn Status", + [validators.Optional(True)], + choices=withdrawn_status_options, + default='') release_date = DateField('Release Date', [validators.Optional(True)]) - #release_year + release_year = IntegerField('Release Year', + [validators.Optional(True), valid_year]) doi = StringField('DOI', [validators.Regexp(r'^10\..*\/.*', message="DOI must be valid"), validators.Optional(True)]) @@ -104,7 +137,8 @@ class ReleaseEntityForm(EntityEditForm): issue = StringField('Issue') pages = StringField('Pages') publisher = StringField('Publisher (optional)') - language = StringField('Language (code)') + language = StringField('Language (code)', + [validators.Optional(True), valid_2char_ascii]) license_slug = StringField('License (slug)') contribs = FieldList(FormField(ReleaseContribForm)) #refs @@ -150,11 +184,13 @@ class ReleaseEntityForm(EntityEditForm): a = None setattr(re, simple_attr, a) for extid_attr in RELEASE_EXTID_ATTRS: - a = getattr(self, simple_attr).data + a = getattr(self, extid_attr).data # special case blank strings if a == '': a = None - setattr(re.ext_ids, simple_attr, a) + setattr(re.ext_ids, extid_attr, a) + if self.release_date.data: + re.release_year = self.release_date.data.year # bunch of complexity here to preserve old contrib metadata (eg, # affiliation and extra) not included in current forms # TODO: this may be broken; either way needs tests @@ -204,8 +240,9 @@ class ContainerEntityForm(EntityEditForm): issnl = StringField("ISSN-L (linking)") issne = StringField("ISSN (electronic)") issnp = StringField("ISSN (print)") - original_name = StringField("Original Name (native language)") - country = StringField("Country of Publication (ISO code)") + original_name = StringField("Name in Original Language (if different)") + country = StringField("Country of Publication (ISO code)", + [validators.Optional(True), valid_2char_ascii]) wikidata_qid = StringField('Wikidata QID') urls = FieldList( StringField("Container URLs", @@ -413,3 +450,31 @@ class SavePaperNowForm(FlaskForm): ingest_request['link_source'] = 'arxiv' ingest_request['link_source_id'] = release.ext_ids.arxiv return ingest_request + +def valid_toml(form, field): + try: + toml.loads(field.data) + except toml.TomlDecodeError as tpe: + raise ValidationError(tpe) + +class EntityTomlForm(EntityEditForm): + + toml = TextAreaField( + "TOML", + [validators.DataRequired(), + valid_toml, + ], + ) + + @staticmethod + def from_entity(entity): + """ + Initializes form with TOML version of existing entity + """ + etf = EntityTomlForm() + if entity.state == 'active': + pop_fields = ['ident', 'state', 'revision', 'redirect'] + else: + pop_fields = ['ident', 'state'] + etf.toml.data = entity_to_toml(entity, pop_fields=pop_fields) + return etf diff --git a/python/fatcat_web/routes.py b/python/fatcat_web/routes.py index 74805c69..da2bb6cf 100644 --- a/python/fatcat_web/routes.py +++ b/python/fatcat_web/routes.py @@ -227,7 +227,7 @@ def generic_editgroup_entity_view(editgroup_id, entity_type, ident, view_templat entity, edit = generic_get_editgroup_entity(editgroup, entity_type, ident) - if entity.state == "deleted": + if entity.revision is None or entity.state == "deleted": return render_template('deleted_entity.html', entity=entity, entity_type=entity_type, editgroup=editgroup) diff --git a/python/fatcat_web/templates/base.html b/python/fatcat_web/templates/base.html index 18c66077..2c18ec44 100644 --- a/python/fatcat_web/templates/base.html +++ b/python/fatcat_web/templates/base.html @@ -25,6 +25,10 @@ @media only screen and (max-width: 479px) { .mobile-hide{ display: none !important; } } + + .field textarea#toml { + font-family: monospace; + } </style> {% block extra_head %}{% endblock %} </head> diff --git a/python/fatcat_web/templates/container_create.html b/python/fatcat_web/templates/container_create.html index 5786d05d..be8c5671 100644 --- a/python/fatcat_web/templates/container_create.html +++ b/python/fatcat_web/templates/container_create.html @@ -9,13 +9,15 @@ a journal (eg, "New England Journal of Medicine"), conference proceedings, a book series, or a blog. Not all publications are in a container. <form class="ui form" id="create_container_form" method="POST" action="/container/create"> + <p>Experienced users can also use the <a href="/container/create/toml">TOML + creation form</a> to access all metadata fields in a raw format. {% endblock %} {% block edit_form_suffix %} <br><br> <input class="ui primary submit button" type="submit" value="Create Container!"> <p> - <i>New entity will be part of the current editgroup, which needs to be + <i>New container entity will be part of the current editgroup, which needs to be submited and approved before the entity will formally be included in the catalog.</i> </form> diff --git a/python/fatcat_web/templates/container_edit.html b/python/fatcat_web/templates/container_edit.html index 5188ce0d..99f77d53 100644 --- a/python/fatcat_web/templates/container_edit.html +++ b/python/fatcat_web/templates/container_edit.html @@ -7,6 +7,14 @@ <h1 class="ui header">Edit Container Entity</h1> <form class="ui form" id="edit_container_form" method="POST" action="{% if editgroup %}/editgroup/{{ editgroup.editgroup_id }}{% endif %}/container/{{ existing_ident }}/edit"> + + <p>Experienced users can also use the <a href="{% if editgroup + %}/editgroup/{{ editgroup.editgroup_id }}{% endif %}/container/{{ + existing_ident }}/edit/toml">TOML editing form</a> to access all metadata + fields in a raw format. + {% if not editgroup %} + You can also <a href="/container/{{ existing_ident }}/delete">delete this entity</a>. + {% endif %} {% endblock %} <p>See <a href="https://guide.fatcat.wiki/entity_container.html">the catalog diff --git a/python/fatcat_web/templates/deleted_entity.html b/python/fatcat_web/templates/deleted_entity.html index eefc87cf..4c6b14b6 100644 --- a/python/fatcat_web/templates/deleted_entity.html +++ b/python/fatcat_web/templates/deleted_entity.html @@ -30,7 +30,7 @@ <b>Entity Type:</b> <code>{{ entity_type }}</code> </div> -{{ entity_macros.fatcat_bits(entity, entity_type, "") }} +{{ entity_macros.fatcat_bits(entity, entity_type, "", editgroup=editgroup) }} </div> </div> diff --git a/python/fatcat_web/templates/edit_macros.html b/python/fatcat_web/templates/edit_macros.html index a207e51e..d4839373 100644 --- a/python/fatcat_web/templates/edit_macros.html +++ b/python/fatcat_web/templates/edit_macros.html @@ -17,6 +17,23 @@ </div> {%- endmacro %} +{% macro form_toml_field(field, div_classes="") -%} +<h3 class="ui dividing header">Raw Metadata (TOML)</h3> +<div class="field {{ div_classes }} {% if field.errors %}error{% endif %}"> + <p><a href="https://toml.io/en/">TOML</a> is a markup language, similar to + YAML or Wikitext. The schema here is the same as the Fatcat API JSON schema, but + with a syntax that is easier to read and edit by hand. All nested metadata + fields are available here; refer to the fatcat guide for metadata + documentation and style guide, or TOML documentation for syntax notes (eg, + what those double brackets mean, and how to represent lists of authors or + references). + <br> + <br> + {{ field(rows=24) }} + {{ form_field_errors(field) }} +</div> +{%- endmacro %} + {% macro form_field_inline(field, div_classes="") -%} <div class="ui grid"> <div class="three wide column middle aligned right aligned" {# style="padding-right: 0.5rem;" #}> @@ -38,7 +55,7 @@ {% macro editgroup_dropdown(form, editgroup=None, potential_editgroups=None) -%} {% if editgroup %} <p>You are updating an existing un-merged editgroup: <a href="/editgroup/{{ editgroup.editgroup_id}}">{{ editgroup.editgroup_id }}</a>. - <p><b>Description:</b> {{ editgroup.description }} + <p><b>Description:</b> {{ editgroup.description or "" }} {% else %} {% if not potential_editgroups %} <p>You have no un-submitted editgroups in progress; a new one will be diff --git a/python/fatcat_web/templates/editgroup_view.html b/python/fatcat_web/templates/editgroup_view.html index e8146d19..a36dc3e5 100644 --- a/python/fatcat_web/templates/editgroup_view.html +++ b/python/fatcat_web/templates/editgroup_view.html @@ -25,7 +25,7 @@ updated {% endif %} <a href="/editgroup/{{ editgroup.editgroup_id }}/{{ entity_type }}/{{ edit.ident }}">[view edit]</a> - {% if auth_to.edit and not editgroup.changelog_index and not editgroup.submitted and entity_type in ('release', 'file', 'container') %} + {% if auth_to.edit and not editgroup.changelog_index and not editgroup.submitted %} <a href="/editgroup/{{ editgroup.editgroup_id }}/{{ entity_type }}/{{ edit.ident }}/edit" style="color: green;">[re-edit]</a> <form id="submit_edit_delete" method="POST" action="/editgroup/{{ editgroup.editgroup_id }}/{{ entity_type }}/edit/{{ edit.edit_id }}/delete" style="display:inline;"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/> diff --git a/python/fatcat_web/templates/entity_create_toml.html b/python/fatcat_web/templates/entity_create_toml.html new file mode 100644 index 00000000..ec5bc4a2 --- /dev/null +++ b/python/fatcat_web/templates/entity_create_toml.html @@ -0,0 +1,20 @@ +{% extends "entity_edit_toml.html" %} + +{% block edit_form_prefix %} +<div class="ui segment"> +<h1 class="ui header">Create New {{ entity_type }} Entity (TOML mode)</h1> + +<form class="ui form" id="create_entity_form" method="POST" action="/{{ entity_type }}/create/toml"> +{% endblock %} + +{% block edit_form_suffix %} + <br><br> + <input class="ui primary submit button" type="submit" value="Create {{ entity_type }}!"> + <p> + <i>New {{ entity_type }} entity will be part of the current editgroup, which + needs to be submited and approved before the entity will formally be included + in the catalog.</i> +</form> +</div> +{% endblock %} + diff --git a/python/fatcat_web/templates/entity_delete.html b/python/fatcat_web/templates/entity_delete.html new file mode 100644 index 00000000..b2e13af4 --- /dev/null +++ b/python/fatcat_web/templates/entity_delete.html @@ -0,0 +1,49 @@ +{% import "edit_macros.html" as edit_macros %} +{% extends "base.html" %} + +{% block body %} +{% block edit_form_prefix %} + <div class="ui segment"> + <h1 class="ui header">Delete Entity</h1> + + <form class="ui form" id="delete_form" method="POST" action="{% if editgroup and editgroup.editgroup_id %}/editgroup/{{ editgroup.editgroup_id }}{% endif %}/{{ entity_type }}/{{ existing_ident }}/delete"> +{% endblock %} + + <p>See <a href="https://guide.fatcat.wiki/entity_release.html">the catalog + style guide</a> for schema notes, and <a + href="https://guide.fatcat.wiki/editing_quickstart.html">the editing + tutorial</a> if this is your first time making an edit. + + {{ form.hidden_tag() }} + + <h3 class="ui dividing header">Editgroup Metadata</h3> + {{ edit_macros.editgroup_dropdown(form, editgroup, potential_editgroups) }} + + <h3 class="ui dividing header">Submit</h3> + {{ edit_macros.form_field_basic(form.edit_description) }} + This description will be attached to the individual edit, not to the + editgroup as a whole. + +{% block edit_form_suffix %} + <br><br> + <input class="ui primary submit button" type="submit" value="Update Release!"> + <p> + <i>Deletion will be part of the current editgroup, which needs to be submited and + approved before the change is included in the catalog.</i> +</form> +</div> +{% endblock %} +{% endblock %} + +{% block postscript %} +<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script> +<script> +<!-- Form code --> +$(document).ready(function() { + + // these javascript dropdowns hide the original <input>, which breaks browser + // form focusing (eg, for required fields) :( + $('.ui.dropdown') .dropdown(); +}); +</script> +{% endblock %} diff --git a/python/fatcat_web/templates/entity_edit.html b/python/fatcat_web/templates/entity_edit.html deleted file mode 100644 index 97f7bf46..00000000 --- a/python/fatcat_web/templates/entity_edit.html +++ /dev/null @@ -1,8 +0,0 @@ -{% extends "base.html" %} -{% block body %} - -<h1>Not Implemented</h1> - -<p>Entity editing via the web interface isn't implemented yet. Sorry! - -{% endblock %} diff --git a/python/fatcat_web/templates/entity_edit_toml.html b/python/fatcat_web/templates/entity_edit_toml.html new file mode 100644 index 00000000..64768d6e --- /dev/null +++ b/python/fatcat_web/templates/entity_edit_toml.html @@ -0,0 +1,55 @@ +{% import "edit_macros.html" as edit_macros %} +{% extends "base.html" %} + +{% block body %} +{% block edit_form_prefix %} + <div class="ui segment"> + <h1 class="ui header">Edit Entity (TOML mode)</h1> + + <form class="ui form" id="edit_toml_form" method="POST" action="{% if editgroup and editgroup.editgroup_id %}/editgroup/{{ editgroup.editgroup_id }}{% endif %}/{{ entity_type }}/{{ existing_ident }}/edit/toml"> + + {% if not editgroup %} + <p>You can also <a href="/{{ entity_type }}/{{ existing_ident }}/delete">delete this entity</a>. + {% endif %} +{% endblock %} + + <p>See <a href="https://guide.fatcat.wiki/entity_release.html">the catalog + style guide</a> for schema notes, and <a + href="https://guide.fatcat.wiki/editing_quickstart.html">the editing + tutorial</a> if this is your first time making an edit. + + {{ form.hidden_tag() }} + + <h3 class="ui dividing header">Editgroup Metadata</h3> + {{ edit_macros.editgroup_dropdown(form, editgroup, potential_editgroups) }} + + {{ edit_macros.form_toml_field(form.toml, "required") }} + + <h3 class="ui dividing header">Submit</h3> + {{ edit_macros.form_field_basic(form.edit_description) }} + This description will be attached to the individual edit, not to the + editgroup as a whole. + +{% block edit_form_suffix %} + <br><br> + <input class="ui primary submit button" type="submit" value="Update Release!"> + <p> + <i>Edit will be part of the current editgroup, which needs to be submited and + approved before the change is included in the catalog.</i> +</form> +</div> +{% endblock %} +{% endblock %} + +{% block postscript %} +<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script> +<script> +<!-- Form code --> +$(document).ready(function() { + + // these javascript dropdowns hide the original <input>, which breaks browser + // form focusing (eg, for required fields) :( + $('.ui.dropdown') .dropdown(); +}); +</script> +{% endblock %} diff --git a/python/fatcat_web/templates/entity_macros.html b/python/fatcat_web/templates/entity_macros.html index 718c071c..0ce646bf 100644 --- a/python/fatcat_web/templates/entity_macros.html +++ b/python/fatcat_web/templates/entity_macros.html @@ -33,7 +33,7 @@ {% if entity.state %} State is "{{ entity.state }}". {% endif %} - {% if entity.state != "deleted" %} + {% if entity.revision %} Revision: <br><small><code><a href="/{{ entity_type }}/rev/{{ entity.revision }}">{{ entity.revision }}</a></code></small> {% endif %} @@ -43,11 +43,13 @@ {%- else -%} https://api.{{ config.FATCAT_DOMAIN }} {%- endif -%} - /v0/{{ entity_type }} - {%- if entity.ident -%} - /{{ entity.ident }} + /v0 + {%- if editgroup and entity.ident -%} + /editgroup/{{ editgroup.editgroup_id }}{# /{{ entity_type }}/{{ entity.ident }} #} + {%- elif entity.ident -%} + /{{ entity_type }}/{{ entity.ident }} {%- elif entity.revision -%} - /rev/{{ entity.revision }} + /{{ entity_type }}/rev/{{ entity.revision }} {% endif %} {% if expand %}?expand={{ expand}}{% endif %}"> As JSON object via API diff --git a/python/fatcat_web/templates/file_create.html b/python/fatcat_web/templates/file_create.html index a7c99b96..affcfb6e 100644 --- a/python/fatcat_web/templates/file_create.html +++ b/python/fatcat_web/templates/file_create.html @@ -5,13 +5,15 @@ <h1 class="ui header">Create New File Entity</h1> <form class="ui form" id="create_file_form" method="POST" action="/file/create"> + <p>Experienced users can also use the <a href="/file/create/toml">TOML + creation form</a> to access all metadata fields in a raw format. {% endblock %} {% block edit_form_suffix %} <br><br> <input class="ui primary submit button" type="submit" value="Create File!"> <p> - <i>New entity will be part of the current editgroup, which needs to be + <i>New file entity will be part of the current editgroup, which needs to be submited and approved before the entity will formally be included in the catalog.</i> </form> diff --git a/python/fatcat_web/templates/file_edit.html b/python/fatcat_web/templates/file_edit.html index e8a421b3..745b0c41 100644 --- a/python/fatcat_web/templates/file_edit.html +++ b/python/fatcat_web/templates/file_edit.html @@ -7,6 +7,14 @@ <h1 class="ui header">Edit File Entity</h1> <form class="ui form" id="edit_file_form" method="POST" action="{% if editgroup %}/editgroup/{{ editgroup.editgroup_id }}{% endif %}/file/{{ existing_ident }}/edit"> + + <p>Experienced users can also use the <a href="{% if editgroup + %}/editgroup/{{ editgroup.editgroup_id }}{% endif %}/file/{{ + existing_ident }}/edit/toml">TOML editing form</a> to access all metadata + fields in a raw format. + {% if not editgroup %} + You can also <a href="/file/{{ existing_ident }}/delete">delete this entity</a>. + {% endif %} {% endblock %} <p>See <a href="https://guide.fatcat.wiki/entity_file.html">the catalog diff --git a/python/fatcat_web/templates/home.html b/python/fatcat_web/templates/home.html index 4557e212..de32d6a4 100644 --- a/python/fatcat_web/templates/home.html +++ b/python/fatcat_web/templates/home.html @@ -171,11 +171,10 @@ <tr><td><b>Creator</b> <br>authors, editors, translators + <td><a href="/creator/create">Create</a> {% if config.FATCAT_DOMAIN == 'fatcat.wiki' %} - <td> <td><a href="/creator/iimvc523xbhqlav6j3sbthuehu">Author</a> {% else %} - <td><!-- <a href="/creator/create">Create</a> --> <td><a href="/creator/iimvc523xbhqlav6j3sbthuehu">Author</a> (prod) <br><a href="/creator/aaaaaaaaaaaaaircaaaaaaaaai">Dummy</a> <br><a href="/creator/aaaaaaaaaaaaaircaaaaaaaaam">Realistic</a> @@ -206,11 +205,10 @@ </form> <tr><td><b>File Set</b> <br>datasets, suplementary materials + <td><a href="/fileset/create">Create</a> {% if config.FATCAT_DOMAIN == 'fatcat.wiki' %} - <td> <td><a href="/fileset/ho376wmdanckpp66iwfs7g22ne">Dataset</a> {% else %} - <td> <td><a href="/fileset/ho376wmdanckpp66iwfs7g22ne">Dataset</a> (prod) <br><a href="/fileset/aaaaaaaaaaaaaztgaaaaaaaaai">Dummy</a> <br><a href="/fileset/aaaaaaaaaaaaaztgaaaaaaaaam">Realistic</a> @@ -218,12 +216,11 @@ <td> <tr><td><b>Web Capture</b> <br>HTML and interactive articles, blog posts + <td><a href="/webcapture/create">Create</a> {% if config.FATCAT_DOMAIN == 'fatcat.wiki' %} - <td> <td><a href="/webcapture/z7uaeatyvfgwdpuxtrdu4okqii">D-Lib</a> <br><a href="/webcapture/5l2pubtfefbmdnqws2izccqlpm">Blog Post</a> {% else %} - <td> <td><a href="/webcapture/z7uaeatyvfgwdpuxtrdu4okqii">D-Lib</a> (prod) <br><a href="/webcapture/aaaaaaaaaaaaa53xaaaaaaaaai">Dummy</a> <br><a href="/webcapture/aaaaaaaaaaaaa53xaaaaaaaaam">Realistic</a> @@ -231,11 +228,10 @@ <td> <tr><td><b>Work</b> <br>for grouping Releases + <td><a href="/work/create">Create</a> {% if config.FATCAT_DOMAIN == 'fatcat.wiki' %} - <td> <td><a href="/work/ftl6xv267vb6xfech3khri3nwa">Paper</a> {% else %} - <td> <td><a href="/work/ftl6xv267vb6xfech3khri3nwa">Paper</a> (prod) <br><a href="/work/aaaaaaaaaaaaavkvaaaaaaaaai">Dummy</a> <br><a href="/work/aaaaaaaaaaaaavkvaaaaaaaaam">Realistic</a> diff --git a/python/fatcat_web/templates/release_create.html b/python/fatcat_web/templates/release_create.html index 5ec2efe5..4f5dabd7 100644 --- a/python/fatcat_web/templates/release_create.html +++ b/python/fatcat_web/templates/release_create.html @@ -5,13 +5,15 @@ <h1 class="ui header">Create New Release Entity</h1> <form class="ui form" id="create_release_form" method="POST" action="/release/create"> + <p>Experienced users can also use the <a href="/release/create/toml">TOML + creation form</a> to access all metadata fields in a raw format. {% endblock %} {% block edit_form_suffix %} <br><br> <input class="ui primary submit button" type="submit" value="Create Release!"> <p> - <i>New entity will be part of the current editgroup, which needs to be + <i>New release entity will be part of the current editgroup, which needs to be submited and approved before the entity will formally be included in the catalog.</i> </form> diff --git a/python/fatcat_web/templates/release_edit.html b/python/fatcat_web/templates/release_edit.html index a4a7e56f..c26c9850 100644 --- a/python/fatcat_web/templates/release_edit.html +++ b/python/fatcat_web/templates/release_edit.html @@ -7,6 +7,14 @@ <h1 class="ui header">Edit Release Entity</h1> <form class="ui form" id="edit_release_form" method="POST" action="{% if editgroup %}/editgroup/{{ editgroup.editgroup_id }}{% endif %}/release/{{ existing_ident }}/edit"> + + <p>Experienced users can also use the <a href="{% if editgroup + %}/editgroup/{{ editgroup.editgroup_id }}{% endif %}/release/{{ + existing_ident }}/edit/toml">TOML editing form</a> to access all metadata + fields in a raw format. + {% if not editgroup %} + You can also <a href="/release/{{ existing_ident }}/delete">delete this entity</a>. + {% endif %} {% endblock %} <p>See <a href="https://guide.fatcat.wiki/entity_release.html">the catalog @@ -14,6 +22,7 @@ href="https://guide.fatcat.wiki/editing_quickstart.html">the editing tutorial</a> if this is your first time making an edit. + {{ form.hidden_tag() }} <h3 class="ui dividing header">Editgroup Metadata</h3> @@ -21,21 +30,22 @@ <br> <h3 class="ui dividing header">The Basics</h3> + + {{ edit_macros.form_field_inline(form.release_type, "required") }} + {{ edit_macros.form_field_inline(form.title, "required") }} + {{ edit_macros.form_field_inline(form.original_title) }} + <div class="ui grid"> <div class="three wide column" style="padding-bottom: 0px;"></div> <div class="twelve wide column" style="padding-bottom: 0px;"> <div class="ui equal width fields"> - {{ edit_macros.form_field_basic(form.release_type, "required") }} - {{ edit_macros.form_field_basic(form.release_stage) }} + {{ edit_macros.form_field_basic(form.release_year) }} + {{ edit_macros.form_field_basic(form.release_date) }} </div> </div> <div class="one wide column" style="padding-bottom: 0px;"></div> </div> - {{ edit_macros.form_field_inline(form.title, "required") }} - {{ edit_macros.form_field_inline(form.original_title) }} - {{ edit_macros.form_field_inline(form.work_id) }} - {{ edit_macros.form_field_inline(form.release_date) }} <div class="ui grid"> <div class="three wide column" style="padding-bottom: 0px;"></div> <div class="twelve wide column" style="padding-bottom: 0px;"> @@ -47,6 +57,19 @@ <div class="one wide column" style="padding-bottom: 0px;"></div> </div> + <div class="ui grid"> + <div class="three wide column" style="padding-bottom: 0px;"></div> + <div class="twelve wide column" style="padding-bottom: 0px;"> + <div class="ui equal width fields"> + {{ edit_macros.form_field_basic(form.release_stage) }} + {{ edit_macros.form_field_basic(form.withdrawn_status) }} + </div> + </div> + <div class="one wide column" style="padding-bottom: 0px;"></div> + </div> + + {{ edit_macros.form_field_inline(form.work_id) }} + <br> <h3 class="ui dividing header">Contributors</h3> <div class="list-group" id="contrib_list" name="contrib_list"> diff --git a/python/tests/transform_toml.py b/python/tests/transform_toml.py new file mode 100644 index 00000000..d12ba027 --- /dev/null +++ b/python/tests/transform_toml.py @@ -0,0 +1,22 @@ + +import json + +from fatcat_tools import * +from fatcat_openapi_client import * +from import_crossref import crossref_importer +from fixtures import * + + +def test_basic_toml(crossref_importer): + with open('tests/files/crossref-works.single.json', 'r') as f: + # not a single line + raw = json.loads(f.read()) + r = crossref_importer.parse_record(raw) + r.state = 'active' + toml_str = entity_to_toml(r) + r2 = entity_from_toml(toml_str, ReleaseEntity) + assert r == r2 + + toml_str = entity_to_toml(r, pop_fields=['ident', 'revision', 'blah', 'extra']) + r3 = entity_from_toml(toml_str, ReleaseEntity) + assert r != r3 diff --git a/python/tests/web_editing.py b/python/tests/web_editing.py index 17f4f5ae..fb8b3f93 100644 --- a/python/tests/web_editing.py +++ b/python/tests/web_editing.py @@ -2,7 +2,7 @@ from fixtures import * -def test_web_release_create_merge(app_admin, api): +def test_web_release_create_accept(app_admin, api): eg = quick_eg(api) @@ -24,6 +24,18 @@ def test_web_release_create_merge(app_admin, api): #assert b'badmojo' in rv.data assert b'Not a valid choice' in rv.data + # bad wikidata QID + rv = app_admin.post('/release/create', + data={ + 'editgroup_id': eg.editgroup_id, + 'release_type': 'article-journal', + 'release_stage': 'published', + 'title': 'something bogus', + 'wikidata_qid': '884', + }, + follow_redirects=True) + assert rv.status_code == 400 + # ok/valid submit rv = app_admin.post('/release/create', data={ @@ -31,9 +43,11 @@ def test_web_release_create_merge(app_admin, api): 'release_type': 'article-journal', 'release_stage': 'published', 'title': 'something bogus', + 'doi': '10.1234/999999', }, follow_redirects=True) assert rv.status_code == 200 + assert b'10.1234/999999' in rv.data rv = app_admin.get('/editgroup/{}'.format(eg.editgroup_id)) assert rv.status_code == 200 @@ -129,18 +143,117 @@ def test_web_file_create(app_admin, api): follow_redirects=True) assert rv.status_code == 200 +def test_web_file_toml_create(app_admin, api): -def test_web_edit_get(app_admin): + eg = quick_eg(api) - # these are all existing entities - rv = app_admin.get('/release/aaaaaaaaaaaaarceaaaaaaaaai/edit') + # bogus/bad submit + rv = app_admin.post('/file/create/toml', + data={ + 'editgroup_id': eg.editgroup_id, + }, + follow_redirects=True) + assert rv.status_code == 400 + + # ok/valid submit + rv = app_admin.post('/file/create/toml', + data={ + 'editgroup_id': eg.editgroup_id, + 'toml': """ +size = 12345 +sha1 = "45be56a396c4d03faaa41e055170c23534dec736" + """, + }, + follow_redirects=True) assert rv.status_code == 200 - assert b'A bigger example' in rv.data - rv = app_admin.get('/file/aaaaaaaaaaaaamztaaaaaaaaam/edit') + # upper-case SHA-1 + rv = app_admin.post('/file/create/toml', + data={ + 'editgroup_id': eg.editgroup_id, + 'toml': """ +size = 12345 +sha1 = "45BE56A396C4D03FAAA41E055170C23534DEC736" + """, + }, + follow_redirects=True) + assert rv.status_code == 400 + +def test_web_file_delete(app_admin, api): + + eg = quick_eg(api) + + rv = app_admin.get('/file/aaaaaaaaaaaaamztaaaaaaaaam/delete') assert rv.status_code == 200 - assert b'ffc1005680cb620eec4c913437dfabbf311b535cfe16cbaeb2faec1f92afc362' in rv.data - rv = app_admin.get('/container/aaaaaaaaaaaaaeiraaaaaaaaam/edit') + rv = app_admin.post('/file/aaaaaaaaaaaaamztaaaaaaaaam/delete', + data={ + 'editgroup_id': eg.editgroup_id, + }, + follow_redirects=True) assert rv.status_code == 200 - assert b'1549-1277' in rv.data + # NOTE: did not *accept* the deletion edit + +DUMMY_DEMO_ENTITIES = { + 'container': 'aaaaaaaaaaaaaeiraaaaaaaaam', + 'creator': 'aaaaaaaaaaaaaircaaaaaaaaaq', + 'file': 'aaaaaaaaaaaaamztaaaaaaaaam', + 'fileset': 'aaaaaaaaaaaaaztgaaaaaaaaai', + 'webcapture': 'aaaaaaaaaaaaa53xaaaaaaaaai', + 'release': 'aaaaaaaaaaaaarceaaaaaaaaai', + 'work': 'aaaaaaaaaaaaavkvaaaaaaaaai', +} + +def test_web_edit_get(app_admin): + + # these are all existing entities + for entity_type in ['release', 'file', 'container']: + rv = app_admin.get(f'/{entity_type}/{DUMMY_DEMO_ENTITIES[entity_type]}/edit') + assert rv.status_code == 200 + if entity_type == 'release': + assert b'A bigger example' in rv.data + elif entity_type == 'file': + assert b'ffc1005680cb620eec4c913437dfabbf311b535cfe16cbaeb2faec1f92afc362' in rv.data + elif entity_type == 'container': + assert b'1549-1277' in rv.data + + rv = app_admin.get(f'/{entity_type}/{DUMMY_DEMO_ENTITIES[entity_type]}/edit/toml') + assert rv.status_code == 200 + if entity_type == 'release': + assert b'A bigger example' in rv.data + elif entity_type == 'file': + assert b'ffc1005680cb620eec4c913437dfabbf311b535cfe16cbaeb2faec1f92afc362' in rv.data + elif entity_type == 'container': + assert b'1549-1277' in rv.data + + # TOML-only endpoints + for entity_type in ['creator', 'fileset', 'webcapture', 'work']: + rv = app_admin.get(f'/{entity_type}/{DUMMY_DEMO_ENTITIES[entity_type]}/edit') + assert rv.status_code == 302 + + rv = app_admin.get(f'/{entity_type}/{DUMMY_DEMO_ENTITIES[entity_type]}/edit/toml') + assert rv.status_code == 200 + + +def test_web_create_get(app_admin): + + for entity_type in ['release', 'file', 'container']: + rv = app_admin.get(f'/{entity_type}/create') + assert rv.status_code == 200 + + rv = app_admin.get(f'/{entity_type}/create/toml') + assert rv.status_code == 200 + + # these are TOML only + for entity_type in ['creator', 'fileset', 'webcapture', 'work']: + rv = app_admin.get(f'/{entity_type}/create') + assert rv.status_code == 302 + + rv = app_admin.get(f'/{entity_type}/create/toml') + assert rv.status_code == 200 + +def test_web_edit_delete(app_admin): + + for entity_type in DUMMY_DEMO_ENTITIES.keys(): + rv = app_admin.get(f'/{entity_type}/{DUMMY_DEMO_ENTITIES[entity_type]}/delete') + assert rv.status_code == 200 diff --git a/python/tests/web_entity_views.py b/python/tests/web_entity_views.py index b01bd815..7b973ef2 100644 --- a/python/tests/web_entity_views.py +++ b/python/tests/web_entity_views.py @@ -210,9 +210,9 @@ def test_web_creator(app): rv = app.get('/creator/aaaaaaaaaaaaaircaaaaaaaaai') assert rv.status_code == 200 rv = app.get('/creator/aaaaaaaaaaaaaircaaaaaaaaai/edit') - assert rv.status_code == 404 + assert rv.status_code == 302 rv = app.get('/creator/create') - assert rv.status_code == 404 + assert rv.status_code == 302 def test_web_file(app): @@ -266,9 +266,9 @@ def test_web_fileset(app): rv = app.get('/fileset/aaaaaaaaaaaaaztgaaaaaaaaai') assert rv.status_code == 200 rv = app.get('/fileset/aaaaaaaaaaaaaztgaaaaaaaaai/edit') - assert rv.status_code == 404 + assert rv.status_code == 302 rv = app.get('/fileset/create') - assert rv.status_code == 404 + assert rv.status_code == 302 def test_web_webcatpure(app): @@ -277,9 +277,9 @@ def test_web_webcatpure(app): rv = app.get('/webcapture/aaaaaaaaaaaaa53xaaaaaaaaai') assert rv.status_code == 200 rv = app.get('/webcapture/aaaaaaaaaaaaa53xaaaaaaaaai/edit') - assert rv.status_code == 404 + assert rv.status_code == 302 rv = app.get('/webcapture/create') - assert rv.status_code == 404 + assert rv.status_code == 302 def test_web_release(app): @@ -376,6 +376,6 @@ def test_web_work(app): rv = app.get('/work/aaaaaaaaaaaaavkvaaaaaaaaai') assert rv.status_code == 200 rv = app.get('/work/aaaaaaaaaaaaavkvaaaaaaaaai/edit') - assert rv.status_code == 404 + assert rv.status_code == 302 rv = app.get('/work/create') - assert rv.status_code == 404 + assert rv.status_code == 302 |