1
This commit is contained in:
14
Pipfile
Normal file
14
Pipfile
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[[source]]
|
||||||
|
url = "https://pypi.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
name = "pypi"
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
flask = "==3.0.3"
|
||||||
|
flask-sqlalchemy = "==3.1.1"
|
||||||
|
sqlalchemy = "==2.0.32"
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
|
||||||
|
[requires]
|
||||||
|
python_version = "3.14"
|
||||||
238
Pipfile.lock
generated
Normal file
238
Pipfile.lock
generated
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"hash": {
|
||||||
|
"sha256": "5382929510634dfb95374f60672a92a30ab672574badbd58646fd42cf1cd545c"
|
||||||
|
},
|
||||||
|
"pipfile-spec": 6,
|
||||||
|
"requires": {
|
||||||
|
"python_version": "3.14"
|
||||||
|
},
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"name": "pypi",
|
||||||
|
"url": "https://pypi.org/simple",
|
||||||
|
"verify_ssl": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"blinker": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf",
|
||||||
|
"sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==1.9.0"
|
||||||
|
},
|
||||||
|
"click": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2",
|
||||||
|
"sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.10'",
|
||||||
|
"version": "==8.3.3"
|
||||||
|
},
|
||||||
|
"flask": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3",
|
||||||
|
"sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
|
"version": "==3.0.3"
|
||||||
|
},
|
||||||
|
"flask-sqlalchemy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4ba4be7f419dc72f4efd8802d69974803c37259dd42f3913b0dcf75c9447e0a0",
|
||||||
|
"sha256:e4b68bb881802dda1a7d878b2fc84c06d1ee57fb40b874d3dc97dabfa36b8312"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
|
"version": "==3.1.1"
|
||||||
|
},
|
||||||
|
"itsdangerous": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef",
|
||||||
|
"sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
|
"version": "==2.2.0"
|
||||||
|
},
|
||||||
|
"jinja2": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d",
|
||||||
|
"sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==3.1.6"
|
||||||
|
},
|
||||||
|
"markupsafe": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f",
|
||||||
|
"sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a",
|
||||||
|
"sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf",
|
||||||
|
"sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19",
|
||||||
|
"sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf",
|
||||||
|
"sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c",
|
||||||
|
"sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175",
|
||||||
|
"sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219",
|
||||||
|
"sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb",
|
||||||
|
"sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6",
|
||||||
|
"sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab",
|
||||||
|
"sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26",
|
||||||
|
"sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1",
|
||||||
|
"sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce",
|
||||||
|
"sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218",
|
||||||
|
"sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634",
|
||||||
|
"sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695",
|
||||||
|
"sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad",
|
||||||
|
"sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73",
|
||||||
|
"sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c",
|
||||||
|
"sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe",
|
||||||
|
"sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa",
|
||||||
|
"sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559",
|
||||||
|
"sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa",
|
||||||
|
"sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37",
|
||||||
|
"sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758",
|
||||||
|
"sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f",
|
||||||
|
"sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8",
|
||||||
|
"sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d",
|
||||||
|
"sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c",
|
||||||
|
"sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97",
|
||||||
|
"sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a",
|
||||||
|
"sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19",
|
||||||
|
"sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9",
|
||||||
|
"sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9",
|
||||||
|
"sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc",
|
||||||
|
"sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2",
|
||||||
|
"sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4",
|
||||||
|
"sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354",
|
||||||
|
"sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50",
|
||||||
|
"sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698",
|
||||||
|
"sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9",
|
||||||
|
"sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b",
|
||||||
|
"sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc",
|
||||||
|
"sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115",
|
||||||
|
"sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e",
|
||||||
|
"sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485",
|
||||||
|
"sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f",
|
||||||
|
"sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12",
|
||||||
|
"sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025",
|
||||||
|
"sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009",
|
||||||
|
"sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d",
|
||||||
|
"sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b",
|
||||||
|
"sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a",
|
||||||
|
"sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5",
|
||||||
|
"sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f",
|
||||||
|
"sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d",
|
||||||
|
"sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1",
|
||||||
|
"sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287",
|
||||||
|
"sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6",
|
||||||
|
"sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f",
|
||||||
|
"sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581",
|
||||||
|
"sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed",
|
||||||
|
"sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b",
|
||||||
|
"sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c",
|
||||||
|
"sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026",
|
||||||
|
"sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8",
|
||||||
|
"sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676",
|
||||||
|
"sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6",
|
||||||
|
"sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e",
|
||||||
|
"sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d",
|
||||||
|
"sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d",
|
||||||
|
"sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01",
|
||||||
|
"sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7",
|
||||||
|
"sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419",
|
||||||
|
"sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795",
|
||||||
|
"sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1",
|
||||||
|
"sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5",
|
||||||
|
"sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d",
|
||||||
|
"sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42",
|
||||||
|
"sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe",
|
||||||
|
"sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda",
|
||||||
|
"sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e",
|
||||||
|
"sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737",
|
||||||
|
"sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523",
|
||||||
|
"sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591",
|
||||||
|
"sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc",
|
||||||
|
"sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a",
|
||||||
|
"sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==3.0.3"
|
||||||
|
},
|
||||||
|
"sqlalchemy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:01438ebcdc566d58c93af0171c74ec28efe6a29184b773e378a385e6215389da",
|
||||||
|
"sha256:0c1c9b673d21477cec17ab10bc4decb1322843ba35b481585facd88203754fc5",
|
||||||
|
"sha256:0c9045ecc2e4db59bfc97b20516dfdf8e41d910ac6fb667ebd3a79ea54084619",
|
||||||
|
"sha256:0d322cc9c9b2154ba7e82f7bf25ecc7c36fbe2d82e2933b3642fc095a52cfc78",
|
||||||
|
"sha256:0ef18a84e5116340e38eca3e7f9eeaaef62738891422e7c2a0b80feab165905f",
|
||||||
|
"sha256:1467940318e4a860afd546ef61fefb98a14d935cd6817ed07a228c7f7c62f389",
|
||||||
|
"sha256:14e09e083a5796d513918a66f3d6aedbc131e39e80875afe81d98a03312889e6",
|
||||||
|
"sha256:167e7497035c303ae50651b351c28dc22a40bb98fbdb8468cdc971821b1ae533",
|
||||||
|
"sha256:19d98f4f58b13900d8dec4ed09dd09ef292208ee44cc9c2fe01c1f0a2fe440e9",
|
||||||
|
"sha256:21b053be28a8a414f2ddd401f1be8361e41032d2ef5884b2f31d31cb723e559f",
|
||||||
|
"sha256:251f0d1108aab8ea7b9aadbd07fb47fb8e3a5838dde34aa95a3349876b5a1f1d",
|
||||||
|
"sha256:295ff8689544f7ee7e819529633d058bd458c1fd7f7e3eebd0f9268ebc56c2a0",
|
||||||
|
"sha256:2b6be53e4fde0065524f1a0a7929b10e9280987b320716c1509478b712a7688c",
|
||||||
|
"sha256:306fe44e754a91cd9d600a6b070c1f2fadbb4a1a257b8781ccf33c7067fd3e4d",
|
||||||
|
"sha256:31983018b74908ebc6c996a16ad3690301a23befb643093fcfe85efd292e384d",
|
||||||
|
"sha256:328429aecaba2aee3d71e11f2477c14eec5990fb6d0e884107935f7fb6001632",
|
||||||
|
"sha256:3bd1cae7519283ff525e64645ebd7a3e0283f3c038f461ecc1c7b040a0c932a1",
|
||||||
|
"sha256:3cd33c61513cb1b7371fd40cf221256456d26a56284e7d19d1f0b9f1eb7dd7e8",
|
||||||
|
"sha256:3eb6a97a1d39976f360b10ff208c73afb6a4de86dd2a6212ddf65c4a6a2347d5",
|
||||||
|
"sha256:4363ed245a6231f2e2957cccdda3c776265a75851f4753c60f3004b90e69bfeb",
|
||||||
|
"sha256:4488120becf9b71b3ac718f4138269a6be99a42fe023ec457896ba4f80749525",
|
||||||
|
"sha256:49496b68cd190a147118af585173ee624114dfb2e0297558c460ad7495f9dfe2",
|
||||||
|
"sha256:4979dc80fbbc9d2ef569e71e0896990bc94df2b9fdbd878290bd129b65ab579c",
|
||||||
|
"sha256:52fec964fba2ef46476312a03ec8c425956b05c20220a1a03703537824b5e8e1",
|
||||||
|
"sha256:5954463675cb15db8d4b521f3566a017c8789222b8316b1e6934c811018ee08b",
|
||||||
|
"sha256:62e23d0ac103bcf1c5555b6c88c114089587bc64d048fef5bbdb58dfd26f96da",
|
||||||
|
"sha256:6bab3db192a0c35e3c9d1560eb8332463e29e5507dbd822e29a0a3c48c0a8d92",
|
||||||
|
"sha256:6c742be912f57586ac43af38b3848f7688863a403dfb220193a882ea60e1ec3a",
|
||||||
|
"sha256:723a40ee2cc7ea653645bd4cf024326dea2076673fc9d3d33f20f6c81db83e1d",
|
||||||
|
"sha256:78c03d0f8a5ab4f3034c0e8482cfcc415a3ec6193491cfa1c643ed707d476f16",
|
||||||
|
"sha256:7d6ba0497c1d066dd004e0f02a92426ca2df20fac08728d03f67f6960271feec",
|
||||||
|
"sha256:7dd8583df2f98dea28b5cd53a1beac963f4f9d087888d75f22fcc93a07cf8d84",
|
||||||
|
"sha256:85a01b5599e790e76ac3fe3aa2f26e1feba56270023d6afd5550ed63c68552b3",
|
||||||
|
"sha256:8a37e4d265033c897892279e8adf505c8b6b4075f2b40d77afb31f7185cd6ecd",
|
||||||
|
"sha256:8bd63d051f4f313b102a2af1cbc8b80f061bf78f3d5bd0843ff70b5859e27924",
|
||||||
|
"sha256:916a798f62f410c0b80b63683c8061f5ebe237b0f4ad778739304253353bc1cb",
|
||||||
|
"sha256:9365a3da32dabd3e69e06b972b1ffb0c89668994c7e8e75ce21d3e5e69ddef28",
|
||||||
|
"sha256:99db65e6f3ab42e06c318f15c98f59a436f1c78179e6a6f40f529c8cc7100b22",
|
||||||
|
"sha256:aaf04784797dcdf4c0aa952c8d234fa01974c4729db55c45732520ce12dd95b4",
|
||||||
|
"sha256:acd9b73c5c15f0ec5ce18128b1fe9157ddd0044abc373e6ecd5ba376a7e5d961",
|
||||||
|
"sha256:ada0102afff4890f651ed91120c1120065663506b760da4e7823913ebd3258be",
|
||||||
|
"sha256:b178e875a7a25b5938b53b006598ee7645172fccafe1c291a706e93f48499ff5",
|
||||||
|
"sha256:b27dfb676ac02529fb6e343b3a482303f16e6bc3a4d868b73935b8792edb52d0",
|
||||||
|
"sha256:b8afd5b26570bf41c35c0121801479958b4446751a3971fb9a480c1afd85558e",
|
||||||
|
"sha256:bf2360a5e0f7bd75fa80431bf8ebcfb920c9f885e7956c7efde89031695cafb8",
|
||||||
|
"sha256:c1b88cc8b02b6a5f0efb0345a03672d4c897dc7d92585176f88c67346f565ea8",
|
||||||
|
"sha256:c41a2b9ca80ee555decc605bd3c4520cc6fef9abde8fd66b1cf65126a6922d65",
|
||||||
|
"sha256:c750987fc876813f27b60d619b987b057eb4896b81117f73bb8d9918c14f1cad",
|
||||||
|
"sha256:e567a8793a692451f706b363ccf3c45e056b67d90ead58c3bc9471af5d212202"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==2.0.32"
|
||||||
|
},
|
||||||
|
"typing-extensions": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466",
|
||||||
|
"sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==4.15.0"
|
||||||
|
},
|
||||||
|
"werkzeug": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50",
|
||||||
|
"sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==3.1.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"develop": {}
|
||||||
|
}
|
||||||
206
app.py
206
app.py
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
from flask import Flask, request, jsonify, render_template_string, redirect
|
from flask import Flask, request, jsonify, render_template_string, redirect, render_template
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
|
|
||||||
@@ -21,8 +21,8 @@ class Student(db.Model):
|
|||||||
current_room = db.Column(db.String(64))
|
current_room = db.Column(db.String(64))
|
||||||
previous_room = db.Column(db.String(64))
|
previous_room = db.Column(db.String(64))
|
||||||
expected_return = db.Column(db.String(64))
|
expected_return = db.Column(db.String(64))
|
||||||
|
expected_room = db.Column(db.String(64))
|
||||||
state = db.Column(db.String(32), default="IN_ROOM")
|
state = db.Column(db.String(32), default="OUT")
|
||||||
|
|
||||||
last_scan = db.Column(db.DateTime)
|
last_scan = db.Column(db.DateTime)
|
||||||
last_reader = db.Column(db.String(64))
|
last_reader = db.Column(db.String(64))
|
||||||
@@ -36,11 +36,13 @@ class Event(db.Model):
|
|||||||
timestamp = db.Column(db.DateTime)
|
timestamp = db.Column(db.DateTime)
|
||||||
|
|
||||||
|
|
||||||
class Bathroom(db.Model):
|
class Room(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
room = db.Column(db.String(64), unique=True)
|
room = db.Column(db.String(64), unique=True)
|
||||||
count = db.Column(db.Integer, default=0)
|
count = db.Column(db.Integer, default=0)
|
||||||
max = db.Column(db.Integer, default=2)
|
max = db.Column(db.Integer, default=2)
|
||||||
|
bathroom = db.Column(db.Boolean, default=True)
|
||||||
|
bathroom_id = db.Column(db.String)
|
||||||
|
|
||||||
|
|
||||||
class Anomaly(db.Model):
|
class Anomaly(db.Model):
|
||||||
@@ -54,10 +56,10 @@ class Anomaly(db.Model):
|
|||||||
# UTIL
|
# UTIL
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
|
|
||||||
COOLDOWN = 3
|
COOLDOWN = 5
|
||||||
|
|
||||||
def now():
|
def now():
|
||||||
return datetime.now(timezone.utc)
|
return datetime.now()
|
||||||
|
|
||||||
|
|
||||||
def recent_scan(student):
|
def recent_scan(student):
|
||||||
@@ -129,61 +131,187 @@ def process_scan(student, room):
|
|||||||
@app.route("/scan", methods=["POST"])
|
@app.route("/scan", methods=["POST"])
|
||||||
def scan():
|
def scan():
|
||||||
data = request.json
|
data = request.json
|
||||||
|
action = data["action"]
|
||||||
|
print(action)
|
||||||
student = Student.query.filter_by(uid=data["uid"]).first()
|
student = Student.query.filter_by(uid=data["uid"]).first()
|
||||||
if not student:
|
if not student:
|
||||||
student = Student(uid=data["uid"], name="Unknown")
|
student = Student(uid=data["uid"], name="Unknown")
|
||||||
db.session.add(student)
|
db.session.add(student)
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
process_scan(student, data["room"])
|
else:
|
||||||
|
student = Student.query.filter_by(uid=data["uid"]).first()
|
||||||
|
location = data["location_id"]
|
||||||
|
room = Room.query.filter_by(room=location).first()
|
||||||
|
student.last_reader = location
|
||||||
|
student.last_scan = now()
|
||||||
|
if action == "":
|
||||||
|
if student.state == "out" and "door" in location: # Entering school
|
||||||
|
student.state = "hallway"
|
||||||
|
if student.state == "hallway" and "door" in location: # Leaving school
|
||||||
|
student.state = "out"
|
||||||
|
if student.state == "hallway" and "classroom" in location: # Entering a classroom
|
||||||
|
student.current_room = location
|
||||||
|
student.state = "in class"
|
||||||
|
room.count = room.count + 1
|
||||||
|
if student.state == "in class" and "classroom" in location: # leaving a classroom should also have time check when schedules are implemtned if time is wrong do an anomaly
|
||||||
|
student.state = "hallway"
|
||||||
|
room.count = room.count - 1
|
||||||
|
# anomaly
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
elif action == "bathroom" :
|
||||||
|
room.bathroom = False
|
||||||
|
student.state = "hallway"
|
||||||
|
student.expected_room = room.bathroom_id
|
||||||
|
student.expected_return = location
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify({"status": "ok"})
|
return jsonify({"status": "ok"})
|
||||||
|
|
||||||
|
@app.route("/lights/bathroom/<id>", methods=["GET"])
|
||||||
|
def lightsBathroom(id):
|
||||||
|
|
||||||
|
room = Room.query.filter_by(room=id).first()
|
||||||
|
bathroom = Room.query.filter_by(room=room.bathroom_id).first()
|
||||||
|
if not room:
|
||||||
|
room = Room(room=id)
|
||||||
|
db.session.add(room)
|
||||||
|
db.session.commit()
|
||||||
|
if (room.bathroom and bathroom.count < bathroom.max):
|
||||||
|
code = 202
|
||||||
|
else:
|
||||||
|
code = 200
|
||||||
|
|
||||||
|
|
||||||
|
return jsonify({"status": "ok"}), code
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# ADMIN DASHBOARD
|
# ADMIN DASHBOARD
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
|
|
||||||
ADMIN_HTML = """
|
|
||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<script src="https://unpkg.com/htmx.org"></script>
|
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
|
||||||
</head>
|
|
||||||
<body class="bg-gray-900 text-white p-6">
|
|
||||||
|
|
||||||
<h1 class="text-2xl font-bold">Admin Dashboard</h1>
|
|
||||||
|
|
||||||
<div hx-get="/admin/students" hx-trigger="load, every 3s" hx-swap="innerHTML"></div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"""
|
|
||||||
|
|
||||||
@app.route("/admin")
|
@app.route("/admin")
|
||||||
def admin():
|
def admin():
|
||||||
return ADMIN_HTML
|
return render_template("admin/index.html")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/admin/students")
|
@app.route("/admin/students")
|
||||||
def admin_students():
|
def admin_students():
|
||||||
students = Student.query.all()
|
students = Student.query.all()
|
||||||
|
return render_template(
|
||||||
html = ""
|
"admin/students.html",
|
||||||
for s in students:
|
students=students
|
||||||
html += f"""
|
)
|
||||||
<div class='bg-gray-800 p-3 m-2 rounded'>
|
|
||||||
<b>{s.name}</b> ({s.uid})<br>
|
|
||||||
Room: {s.current_room}<br>
|
|
||||||
State: {s.state}<br>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
|
|
||||||
return html
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/admin/unknown")
|
||||||
|
def admin_unknown():
|
||||||
|
|
||||||
|
students = Student.query.all()
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"admin/unknown_cards.html",
|
||||||
|
students=students
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/admin/rooms")
|
||||||
|
def admin_rooms():
|
||||||
|
|
||||||
|
rooms = Room.query.all()
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"admin/rooms.html",
|
||||||
|
rooms=rooms
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/admin/anomalies")
|
||||||
|
def admin_anomalies():
|
||||||
|
|
||||||
|
anomalies = Anomaly.query.order_by(
|
||||||
|
Anomaly.timestamp.desc()
|
||||||
|
).all()
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"admin/anomalies.html",
|
||||||
|
anomalies=anomalies
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.route("/admin/student/assign", methods=["POST"])
|
||||||
|
def assign_student():
|
||||||
|
|
||||||
|
uid = request.form["uid"]
|
||||||
|
name = request.form["name"]
|
||||||
|
|
||||||
|
student = Student.query.filter_by(
|
||||||
|
uid=uid
|
||||||
|
).first()
|
||||||
|
|
||||||
|
student.name = name
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return admin_unknown()
|
||||||
|
|
||||||
|
@app.route("/admin/student/merge", methods=["POST"])
|
||||||
|
def merge_student():
|
||||||
|
|
||||||
|
old_uid = request.form["old_uid"]
|
||||||
|
new_uid = request.form["new_uid"]
|
||||||
|
|
||||||
|
old_student = Student.query.filter_by(
|
||||||
|
uid=old_uid
|
||||||
|
).first()
|
||||||
|
|
||||||
|
new_student = Student.query.filter_by(
|
||||||
|
uid=new_uid
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not old_student or not new_student:
|
||||||
|
return "Missing student", 404
|
||||||
|
|
||||||
|
# move all events
|
||||||
|
|
||||||
|
events = Event.query.filter_by(
|
||||||
|
uid=new_uid
|
||||||
|
).all()
|
||||||
|
|
||||||
|
for e in events:
|
||||||
|
e.uid = old_uid
|
||||||
|
|
||||||
|
db.session.delete(new_student)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return admin_unknown()
|
||||||
|
|
||||||
|
@app.route(
|
||||||
|
"/admin/room/update/<int:id>",
|
||||||
|
methods=["POST"]
|
||||||
|
)
|
||||||
|
def update_room(id):
|
||||||
|
|
||||||
|
room = Room.query.get(id)
|
||||||
|
|
||||||
|
room.max = int(
|
||||||
|
request.form["max"]
|
||||||
|
)
|
||||||
|
|
||||||
|
room.bathroom_id = request.form[
|
||||||
|
"bathroom_id"
|
||||||
|
]
|
||||||
|
|
||||||
|
room.tracks_bathroom = (
|
||||||
|
"tracks_bathroom"
|
||||||
|
in request.form
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return admin_rooms()
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# TEACHER DASHBOARD
|
# TEACHER DASHBOARD
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
|
|||||||
31
templates/admin/anomalies.html
Normal file
31
templates/admin/anomalies.html
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<div>
|
||||||
|
|
||||||
|
<h2 class="text-2xl font-bold mb-4">
|
||||||
|
Active Anomalies
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
|
||||||
|
{% for a in anomalies %}
|
||||||
|
|
||||||
|
<div class="bg-red-950 border border-red-800 p-4 rounded-xl">
|
||||||
|
|
||||||
|
<div class="font-bold text-lg">
|
||||||
|
{{ a.type }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-gray-300">
|
||||||
|
UID: {{ a.uid }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-gray-400 text-sm">
|
||||||
|
{{ a.timestamp }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
77
templates/admin/index.html
Normal file
77
templates/admin/index.html
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script src="https://unpkg.com/htmx.org"></script>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
|
||||||
|
<title>RFID Admin</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="bg-gray-950 text-white min-h-screen">
|
||||||
|
|
||||||
|
<div class="flex">
|
||||||
|
|
||||||
|
<!-- SIDEBAR -->
|
||||||
|
|
||||||
|
<div class="w-72 bg-gray-900 min-h-screen p-4 border-r border-gray-800">
|
||||||
|
|
||||||
|
<h1 class="text-3xl font-bold mb-6">
|
||||||
|
RFID Admin
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
|
||||||
|
<button
|
||||||
|
hx-get="/admin/students"
|
||||||
|
hx-target="#content"
|
||||||
|
class="w-full bg-gray-800 hover:bg-gray-700 p-3 rounded text-left"
|
||||||
|
>
|
||||||
|
Students
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
hx-get="/admin/unknown"
|
||||||
|
hx-target="#content"
|
||||||
|
class="w-full bg-gray-800 hover:bg-gray-700 p-3 rounded text-left"
|
||||||
|
>
|
||||||
|
Unknown Cards
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
hx-get="/admin/rooms"
|
||||||
|
hx-target="#content"
|
||||||
|
class="w-full bg-gray-800 hover:bg-gray-700 p-3 rounded text-left"
|
||||||
|
>
|
||||||
|
Rooms
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
hx-get="/admin/anomalies"
|
||||||
|
hx-target="#content"
|
||||||
|
class="w-full bg-gray-800 hover:bg-gray-700 p-3 rounded text-left"
|
||||||
|
>
|
||||||
|
Anomalies
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MAIN -->
|
||||||
|
|
||||||
|
<div class="flex-1 p-6">
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="content"
|
||||||
|
hx-get="/admin/students"
|
||||||
|
hx-trigger="load"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
114
templates/admin/rooms.html
Normal file
114
templates/admin/rooms.html
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
<div>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
|
||||||
|
<h2 class="text-2xl font-bold">
|
||||||
|
Rooms
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-3 gap-4">
|
||||||
|
|
||||||
|
{% for r in rooms %}
|
||||||
|
|
||||||
|
<div class="bg-gray-900 p-4 rounded-xl border border-gray-800">
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<div class="font-bold text-xl">
|
||||||
|
{{ r.room }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-gray-400 text-sm">
|
||||||
|
Current Occupancy:
|
||||||
|
{{ r.count }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if r.bathroom %}
|
||||||
|
<span class="bg-blue-700 px-2 py-1 rounded text-sm">
|
||||||
|
Bathroom Tracking
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
hx-post="/admin/room/update/{{ r.id }}"
|
||||||
|
hx-target="#content"
|
||||||
|
class="mt-4 space-y-3"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- MAX OCCUPANCY -->
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<label class="block text-sm mb-1">
|
||||||
|
Max Occupancy
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="max"
|
||||||
|
value="{{ r.max }}"
|
||||||
|
class="bg-gray-800 p-2 rounded w-full"
|
||||||
|
>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- BATHROOM GROUP -->
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<label class="block text-sm mb-1">
|
||||||
|
Bathroom Group ID
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="bathroom_id"
|
||||||
|
value="{{ r.bathroom_id }}"
|
||||||
|
placeholder="example: floor2_west"
|
||||||
|
class="bg-gray-800 p-2 rounded w-full"
|
||||||
|
>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TRACK BATHROOM -->
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="bathroom"
|
||||||
|
{% if r.bathroom %}
|
||||||
|
checked
|
||||||
|
{% endif %}
|
||||||
|
class="w-5 h-5"
|
||||||
|
>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Track Bathroom Usage
|
||||||
|
</label>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="bg-blue-700 hover:bg-blue-600 px-4 py-2 rounded w-full"
|
||||||
|
>
|
||||||
|
Save Room
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
70
templates/admin/students.html
Normal file
70
templates/admin/students.html
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<div>
|
||||||
|
|
||||||
|
<h2 class="text-2xl font-bold mb-4">
|
||||||
|
Students
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-3 gap-4">
|
||||||
|
|
||||||
|
{% for s in students %}
|
||||||
|
|
||||||
|
<div class="bg-gray-900 rounded-xl p-4 border border-gray-800">
|
||||||
|
|
||||||
|
<div class="flex justify-between">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="text-xl font-bold">
|
||||||
|
{{ s.name }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-gray-400">
|
||||||
|
{{ s.uid }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="bg-blue-700 px-2 py-1 rounded">
|
||||||
|
{{ s.state }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 text-sm space-y-1">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
Current Room:
|
||||||
|
<b>{{ s.current_room }}</b>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
Previous Room:
|
||||||
|
<b>{{ s.previous_room }}</b>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
Expected Return:
|
||||||
|
<b>{{ s.expected_return }}</b>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 flex gap-2">
|
||||||
|
|
||||||
|
<button
|
||||||
|
hx-post="/admin/student/reset/{{ s.id }}"
|
||||||
|
hx-target="closest div"
|
||||||
|
class="bg-red-700 hover:bg-red-600 px-3 py-1 rounded"
|
||||||
|
>
|
||||||
|
Reset State
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
85
templates/admin/unknown_cards.html
Normal file
85
templates/admin/unknown_cards.html
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<div>
|
||||||
|
|
||||||
|
<h2 class="text-2xl font-bold mb-4">
|
||||||
|
Unknown RFID Cards
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
|
||||||
|
{% for s in students %}
|
||||||
|
|
||||||
|
{% if s.name == "Unknown" %}
|
||||||
|
|
||||||
|
<div class="bg-gray-900 p-4 rounded-xl border border-gray-800">
|
||||||
|
|
||||||
|
<div class="font-bold text-lg">
|
||||||
|
{{ s.uid }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ASSIGN -->
|
||||||
|
|
||||||
|
<form
|
||||||
|
hx-post="/admin/student/assign"
|
||||||
|
hx-target="#content"
|
||||||
|
class="mt-4 flex gap-2"
|
||||||
|
>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="uid"
|
||||||
|
value="{{ s.uid }}"
|
||||||
|
>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
placeholder="Student Name"
|
||||||
|
class="bg-gray-800 p-2 rounded w-full"
|
||||||
|
>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="bg-green-700 px-4 rounded"
|
||||||
|
>
|
||||||
|
Assign
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- MERGE -->
|
||||||
|
|
||||||
|
<form
|
||||||
|
hx-post="/admin/student/merge"
|
||||||
|
hx-target="#content"
|
||||||
|
class="mt-4 flex gap-2"
|
||||||
|
>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="new_uid"
|
||||||
|
value="{{ s.uid }}"
|
||||||
|
>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="old_uid"
|
||||||
|
placeholder="Old UID"
|
||||||
|
class="bg-gray-800 p-2 rounded w-full"
|
||||||
|
>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="bg-yellow-700 px-4 rounded"
|
||||||
|
>
|
||||||
|
Merge
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user