diff --git a/Pipfile b/Pipfile index 44e04f14ff..c011079549 100644 --- a/Pipfile +++ b/Pipfile @@ -8,11 +8,9 @@ verify_ssl = true [packages] flask = "*" flask-sqlalchemy = "*" -flask-migrate = "*" flask-swagger = "*" psycopg2-binary = "*" python-dotenv = "*" -flask-cors = "*" gunicorn = "*" cloudinary = "*" flask-admin = "*" @@ -20,6 +18,11 @@ typing-extensions = "*" flask-jwt-extended = "==4.6.0" wtforms = "==3.1.2" sqlalchemy = "*" +flask-migrate = "*" +pytz = "*" +icalendar = "*" +requests = "*" +flask-cors = "*" [requires] python_version = "3.13" diff --git a/Pipfile.lock b/Pipfile.lock index b201c3decc..98178aac96 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d2e672e650278aeeee2fe49bd76d76497d8b65a50f8b5dbb121d265cbc6ef4e5" + "sha256": "2360978c575d486c8de4573333bf1f8794f5c566f71c1dfec102921954505bd3" }, "pipfile-spec": 6, "requires": { @@ -18,11 +18,11 @@ "default": { "alembic": { "hashes": [ - "sha256:1acdd7a3a478e208b0503cd73614d5e4c6efafa4e73518bb60e4f2846a37b1c5", - "sha256:496e888245a53adf1498fcab31713a469c65836f8de76e01399aa1c3e90dd213" + "sha256:a88bb7f6e513bd4301ecf4c7f2206fe93f9913f9b48dac3b78babde2d6fe765e", + "sha256:e845dfe090c5ffa7b92593ae6687c5cb1a101e91fa53868497dbd79847f9dbe3" ], - "markers": "python_version >= '3.8'", - "version": "==1.14.1" + "markers": "python_version >= '3.9'", + "version": "==1.16.5" }, "blinker": { "hashes": [ @@ -34,19 +34,104 @@ }, "certifi": { "hashes": [ - "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", - "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe" + "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", + "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5" ], - "markers": "python_version >= '3.6'", - "version": "==2025.1.31" + "markers": "python_version >= '3.7'", + "version": "==2025.8.3" }, - "click": { + "charset-normalizer": { "hashes": [ - "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", - "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a" + "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", + "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", + "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", + "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", + "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", + "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", + "sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c", + "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", + "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", + "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", + "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432", + "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", + "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", + "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", + "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", + "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19", + "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", + "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e", + "sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4", + "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7", + "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312", + "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", + "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", + "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", + "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", + "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99", + "sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b", + "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", + "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", + "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", + "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", + "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", + "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", + "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc", + "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", + "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", + "sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a", + "sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40", + "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", + "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", + "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", + "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", + "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05", + "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", + "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", + "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", + "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", + "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34", + "sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9", + "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", + "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", + "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", + "sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b", + "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", + "sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942", + "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", + "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", + "sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b", + "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", + "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", + "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", + "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", + "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", + "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", + "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", + "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", + "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", + "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", + "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca", + "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", + "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", + "sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb", + "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", + "sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557", + "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", + "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7", + "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", + "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", + "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9" ], "markers": "python_version >= '3.7'", - "version": "==8.1.8" + "version": "==3.4.3" + }, + "click": { + "hashes": [ + "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", + "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4" + ], + "markers": "python_version >= '3.10'", + "version": "==8.3.0" }, "cloudinary": { "hashes": [ @@ -58,11 +143,12 @@ }, "flask": { "hashes": [ - "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac", - "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136" + "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", + "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c" ], "index": "pypi", - "version": "==3.1.0" + "markers": "python_version >= '3.9'", + "version": "==3.1.2" }, "flask-admin": { "hashes": [ @@ -74,11 +160,12 @@ }, "flask-cors": { "hashes": [ - "sha256:6ccb38d16d6b72bbc156c1c3f192bc435bfcc3c2bc864b2df1eb9b2d97b2403c", - "sha256:fa5cb364ead54bbf401a26dbf03030c6b18fb2fcaf70408096a572b409586b0c" + "sha256:c7b2cbfb1a31aa0d2e5341eea03a6805349f7a61647daee1a15c46bbe981494c", + "sha256:d81bcb31f07b0985be7f48406247e9243aced229b7747219160a0559edd678db" ], "index": "pypi", - "version": "==5.0.1" + "markers": "python_version >= '3.9' and python_version < '4.0'", + "version": "==6.0.1" }, "flask-jwt-extended": { "hashes": [ @@ -94,6 +181,7 @@ "sha256:24d8051af161782e0743af1b04a152d007bad9772b2bca67b7ec1e8ceeb3910d" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==4.1.0" }, "flask-sqlalchemy": { @@ -102,6 +190,7 @@ "sha256:e4b68bb881802dda1a7d878b2fc84c06d1ee57fb40b874d3dc97dabfa36b8312" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==3.1.1" }, "flask-swagger": { @@ -114,82 +203,63 @@ }, "greenlet": { "hashes": [ - "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", - "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7", - "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", - "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", - "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", - "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563", - "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83", - "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", - "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", - "sha256:1d3755bcb2e02de341c55b4fca7a745a24a9e7212ac953f6b3a48d117d7257aa", - "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", - "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", - "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", - "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22", - "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9", - "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0", - "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba", - "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3", - "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", - "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", - "sha256:47da355d8687fd65240c364c90a31569a133b7b60de111c255ef5b606f2ae291", - "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", - "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", - "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", - "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", - "sha256:54558ea205654b50c438029505def3834e80f0869a70fb15b871c29b4575ddef", - "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c", - "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", - "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c", - "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", - "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", - "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8", - "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d", - "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", - "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145", - "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80", - "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", - "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e", - "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", - "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1", - "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef", - "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc", - "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", - "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120", - "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437", - "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd", - "sha256:98884ecf2ffb7d7fe6bd517e8eb99d31ff7855a840fa6d0d63cd07c037f6a981", - "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", - "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", - "sha256:a0dfc6c143b519113354e780a50381508139b07d2177cb6ad6a08278ec655798", - "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7", - "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", - "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", - "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", - "sha256:b8da394b34370874b4572676f36acabac172602abf054cbc4ac910219f3340af", - "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", - "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", - "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42", - "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e", - "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81", - "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", - "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617", - "sha256:db32b5348615a04b82240cc67983cb315309e88d444a288934ee6ceaebcad6cc", - "sha256:dcc62f31eae24de7f8dce72134c8651c58000d3b1868e01392baea7c32c247de", - "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111", - "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", - "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", - "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6", - "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", - "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", - "sha256:f1d4aeb8891338e60d1ab6127af1fe45def5259def8094b9c7e34690c8858803", - "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", - "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f" - ], - "markers": "python_version < '3.14' and (platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32'))))))", - "version": "==3.1.1" + "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", + "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", + "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079", + "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", + "sha256:16458c245a38991aa19676900d48bd1a6f2ce3e16595051a4db9d012154e8433", + "sha256:18d9260df2b5fbf41ae5139e1be4e796d99655f023a636cd0e11e6406cca7d58", + "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", + "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", + "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", + "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", + "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", + "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", + "sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d", + "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", + "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", + "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", + "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", + "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", + "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633", + "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", + "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", + "sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31", + "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", + "sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b", + "sha256:671df96c1f23c4a0d4077a325483c1503c96a1b7d9db26592ae770daa41233d4", + "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", + "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c", + "sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98", + "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f", + "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c", + "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590", + "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3", + "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", + "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", + "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", + "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", + "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", + "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", + "sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c", + "sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594", + "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", + "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d", + "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", + "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", + "sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b", + "sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df", + "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", + "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", + "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb", + "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", + "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", + "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", + "sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c", + "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968" + ], + "markers": "python_version >= '3.9'", + "version": "==3.2.4" }, "gunicorn": { "hashes": [ @@ -199,6 +269,23 @@ "index": "pypi", "version": "==23.0.0" }, + "icalendar": { + "hashes": [ + "sha256:7ea1d1b212df685353f74cdc6ec9646bf42fa557d1746ea645ce8779fdfbecdd", + "sha256:a697ce7b678072941e519f2745704fc29d78ef92a2dc53d9108ba6a04aeba466" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==6.3.1" + }, + "idna": { + "hashes": [ + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" + ], + "markers": "python_version >= '3.6'", + "version": "==3.10" + }, "itsdangerous": { "hashes": [ "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", @@ -209,19 +296,19 @@ }, "jinja2": { "hashes": [ - "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", - "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb" + "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", + "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67" ], "markers": "python_version >= '3.7'", - "version": "==3.1.5" + "version": "==3.1.6" }, "mako": { "hashes": [ - "sha256:95920acccb578427a9aa38e37a186b1e43156c87260d7ba18ca63aa4c7cbd3a1", - "sha256:b5d65ff3462870feec922dbccf38f6efb44e5714d7b593a656be86663d8600ac" + "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", + "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59" ], "markers": "python_version >= '3.8'", - "version": "==1.3.9" + "version": "==1.3.10" }, "markupsafe": { "hashes": [ @@ -380,6 +467,14 @@ "markers": "python_version >= '3.9'", "version": "==2.10.1" }, + "python-dateutil": { + "hashes": [ + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "version": "==2.9.0.post0" + }, "python-dotenv": { "hashes": [ "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", @@ -388,6 +483,14 @@ "index": "pypi", "version": "==1.0.1" }, + "pytz": { + "hashes": [ + "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", + "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00" + ], + "index": "pypi", + "version": "==2025.2" + }, "pyyaml": { "hashes": [ "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", @@ -447,6 +550,15 @@ "markers": "python_version >= '3.8'", "version": "==6.0.2" }, + "requests": { + "hashes": [ + "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", + "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==2.32.5" + }, "six": { "hashes": [ "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", @@ -457,82 +569,92 @@ }, "sqlalchemy": { "hashes": [ - "sha256:0398361acebb42975deb747a824b5188817d32b5c8f8aba767d51ad0cc7bb08d", - "sha256:0561832b04c6071bac3aad45b0d3bb6d2c4f46a8409f0a7a9c9fa6673b41bc03", - "sha256:07258341402a718f166618470cde0c34e4cec85a39767dce4e24f61ba5e667ea", - "sha256:0a826f21848632add58bef4f755a33d45105d25656a0c849f2dc2df1c71f6f50", - "sha256:1052723e6cd95312f6a6eff9a279fd41bbae67633415373fdac3c430eca3425d", - "sha256:12d5b06a1f3aeccf295a5843c86835033797fea292c60e72b07bcb5d820e6dd3", - "sha256:12f5c9ed53334c3ce719155424dc5407aaa4f6cadeb09c5b627e06abb93933a1", - "sha256:2a0ef3f98175d77180ffdc623d38e9f1736e8d86b6ba70bff182a7e68bed7727", - "sha256:2f2951dc4b4f990a4b394d6b382accb33141d4d3bd3ef4e2b27287135d6bdd68", - "sha256:3868acb639c136d98107c9096303d2d8e5da2880f7706f9f8c06a7f961961149", - "sha256:386b7d136919bb66ced64d2228b92d66140de5fefb3c7df6bd79069a269a7b06", - "sha256:3d3043375dd5bbcb2282894cbb12e6c559654c67b5fffb462fda815a55bf93f7", - "sha256:3e35d5565b35b66905b79ca4ae85840a8d40d31e0b3e2990f2e7692071b179ca", - "sha256:402c2316d95ed90d3d3c25ad0390afa52f4d2c56b348f212aa9c8d072a40eee5", - "sha256:40310db77a55512a18827488e592965d3dec6a3f1e3d8af3f8243134029daca3", - "sha256:40e9cdbd18c1f84631312b64993f7d755d85a3930252f6276a77432a2b25a2f3", - "sha256:49aa2cdd1e88adb1617c672a09bf4ebf2f05c9448c6dbeba096a3aeeb9d4d443", - "sha256:57dd41ba32430cbcc812041d4de8d2ca4651aeefad2626921ae2a23deb8cd6ff", - "sha256:5dba1cdb8f319084f5b00d41207b2079822aa8d6a4667c0f369fce85e34b0c86", - "sha256:5e1d9e429028ce04f187a9f522818386c8b076723cdbe9345708384f49ebcec6", - "sha256:63178c675d4c80def39f1febd625a6333f44c0ba269edd8a468b156394b27753", - "sha256:6493bc0eacdbb2c0f0d260d8988e943fee06089cd239bd7f3d0c45d1657a70e2", - "sha256:64aa8934200e222f72fcfd82ee71c0130a9c07d5725af6fe6e919017d095b297", - "sha256:665255e7aae5f38237b3a6eae49d2358d83a59f39ac21036413fab5d1e810578", - "sha256:6db316d6e340f862ec059dc12e395d71f39746a20503b124edc255973977b728", - "sha256:70065dfabf023b155a9c2a18f573e47e6ca709b9e8619b2e04c54d5bcf193178", - "sha256:8455aa60da49cb112df62b4721bd8ad3654a3a02b9452c783e651637a1f21fa2", - "sha256:8b0ac78898c50e2574e9f938d2e5caa8fe187d7a5b69b65faa1ea4648925b096", - "sha256:8bf312ed8ac096d674c6aa9131b249093c1b37c35db6a967daa4c84746bc1bc9", - "sha256:92f99f2623ff16bd4aaf786ccde759c1f676d39c7bf2855eb0b540e1ac4530c8", - "sha256:9c8bcad7fc12f0cc5896d8e10fdf703c45bd487294a986903fe032c72201596b", - "sha256:9cd136184dd5f58892f24001cdce986f5d7e96059d004118d5410671579834a4", - "sha256:9eb4fa13c8c7a2404b6a8e3772c17a55b1ba18bc711e25e4d6c0c9f5f541b02a", - "sha256:a2bc4e49e8329f3283d99840c136ff2cd1a29e49b5624a46a290f04dff48e079", - "sha256:a5645cd45f56895cfe3ca3459aed9ff2d3f9aaa29ff7edf557fa7a23515a3725", - "sha256:a9afbc3909d0274d6ac8ec891e30210563b2c8bdd52ebbda14146354e7a69373", - "sha256:aa498d1392216fae47eaf10c593e06c34476ced9549657fca713d0d1ba5f7248", - "sha256:afd776cf1ebfc7f9aa42a09cf19feadb40a26366802d86c1fba080d8e5e74bdd", - "sha256:b335a7c958bc945e10c522c069cd6e5804f4ff20f9a744dd38e748eb602cbbda", - "sha256:b3c4817dff8cef5697f5afe5fec6bc1783994d55a68391be24cb7d80d2dbc3a6", - "sha256:b79ee64d01d05a5476d5cceb3c27b5535e6bb84ee0f872ba60d9a8cd4d0e6579", - "sha256:b87a90f14c68c925817423b0424381f0e16d80fc9a1a1046ef202ab25b19a444", - "sha256:bf89e0e4a30714b357f5d46b6f20e0099d38b30d45fa68ea48589faf5f12f62d", - "sha256:c058b84c3b24812c859300f3b5abf300daa34df20d4d4f42e9652a4d1c48c8a4", - "sha256:c09a6ea87658695e527104cf857c70f79f14e9484605e205217aae0ec27b45fc", - "sha256:c57b8e0841f3fce7b703530ed70c7c36269c6d180ea2e02e36b34cb7288c50c7", - "sha256:c9cea5b756173bb86e2235f2f871b406a9b9d722417ae31e5391ccaef5348f2c", - "sha256:cb39ed598aaf102251483f3e4675c5dd6b289c8142210ef76ba24aae0a8f8aba", - "sha256:e036549ad14f2b414c725349cce0772ea34a7ab008e9cd67f9084e4f371d1f32", - "sha256:e185ea07a99ce8b8edfc788c586c538c4b1351007e614ceb708fd01b095ef33e", - "sha256:e5a4d82bdb4bf1ac1285a68eab02d253ab73355d9f0fe725a97e1e0fa689decb", - "sha256:eae27ad7580529a427cfdd52c87abb2dfb15ce2b7a3e0fc29fbb63e2ed6f8120", - "sha256:ecef029b69843b82048c5b347d8e6049356aa24ed644006c9a9d7098c3bd3bfd", - "sha256:ee3bee874cb1fadee2ff2b79fc9fc808aa638670f28b2145074538d4a6a5028e", - "sha256:f0d3de936b192980209d7b5149e3c98977c3810d401482d05fb6d668d53c1c63", - "sha256:f53c0d6a859b2db58332e0e6a921582a02c1677cc93d4cbb36fdf49709b327b2", - "sha256:f9d57f1b3061b3e21476b0ad5f0397b112b94ace21d1f439f2db472e568178ae" + "sha256:022e436a1cb39b13756cf93b48ecce7aa95382b9cfacceb80a7d263129dfd019", + "sha256:03d73ab2a37d9e40dec4984d1813d7878e01dbdc742448d44a7341b7a9f408c7", + "sha256:07097c0a1886c150ef2adba2ff7437e84d40c0f7dcb44a2c2b9c905ccfc6361c", + "sha256:11b9503fa6f8721bef9b8567730f664c5a5153d25e247aadc69247c4bc605227", + "sha256:11f43c39b4b2ec755573952bbcc58d976779d482f6f832d7f33a8d869ae891bf", + "sha256:13194276e69bb2af56198fef7909d48fd34820de01d9c92711a5fa45497cc7ed", + "sha256:136063a68644eca9339d02e6693932116f6a8591ac013b0014479a1de664e40a", + "sha256:14111d22c29efad445cd5021a70a8b42f7d9152d8ba7f73304c4d82460946aaa", + "sha256:1681c21dd2ccee222c2fe0bef671d1aef7c504087c9c4e800371cfcc8ac966fc", + "sha256:1a113da919c25f7f641ffbd07fbc9077abd4b3b75097c888ab818f962707eb48", + "sha256:1c6d85327ca688dbae7e2b06d7d84cfe4f3fffa5b5f9e21bb6ce9d0e1a0e0e0a", + "sha256:20d81fc2736509d7a2bd33292e489b056cbae543661bb7de7ce9f1c0cd6e7f24", + "sha256:21b27b56eb2f82653168cefe6cb8e970cdaf4f3a6cb2c5e3c3c1cf3158968ff9", + "sha256:21ba7a08a4253c5825d1db389d4299f64a100ef9800e4624c8bf70d8f136e6ed", + "sha256:227119ce0a89e762ecd882dc661e0aa677a690c914e358f0dd8932a2e8b2765b", + "sha256:25b9fc27650ff5a2c9d490c13c14906b918b0de1f8fcbb4c992712d8caf40e83", + "sha256:334f41fa28de9f9be4b78445e68530da3c5fa054c907176460c81494f4ae1f5e", + "sha256:413391b2239db55be14fa4223034d7e13325a1812c8396ecd4f2c08696d5ccad", + "sha256:4286a1139f14b7d70141c67a8ae1582fc2b69105f1b09d9573494eb4bb4b2687", + "sha256:44337823462291f17f994d64282a71c51d738fc9ef561bf265f1d0fd9116a782", + "sha256:46293c39252f93ea0910aababa8752ad628bcce3a10d3f260648dd472256983f", + "sha256:4bf0edb24c128b7be0c61cd17eef432e4bef507013292415f3fb7023f02b7d4b", + "sha256:4d3d9b904ad4a6b175a2de0738248822f5ac410f52c2fd389ada0b5262d6a1e3", + "sha256:4e6aeb2e0932f32950cf56a8b4813cb15ff792fc0c9b3752eaf067cfe298496a", + "sha256:4fb1a8c5438e0c5ea51afe9c6564f951525795cf432bed0c028c1cb081276685", + "sha256:529064085be2f4d8a6e5fab12d36ad44f1909a18848fcfbdb59cc6d4bbe48efe", + "sha256:52d9b73b8fb3e9da34c2b31e6d99d60f5f99fd8c1225c9dad24aeb74a91e1d29", + "sha256:5cda6b51faff2639296e276591808c1726c4a77929cfaa0f514f30a5f6156921", + "sha256:5d79f9fdc9584ec83d1b3c75e9f4595c49017f5594fee1a2217117647225d738", + "sha256:61f964a05356f4bca4112e6334ed7c208174511bd56e6b8fc86dad4d024d4185", + "sha256:6772e3ca8a43a65a37c88e2f3e2adfd511b0b1da37ef11ed78dea16aeae85bd9", + "sha256:6e2bf13d9256398d037fef09fd8bf9b0bf77876e22647d10761d35593b9ac547", + "sha256:70322986c0c699dca241418fcf18e637a4369e0ec50540a2b907b184c8bca069", + "sha256:788bfcef6787a7764169cfe9859fe425bf44559619e1d9f56f5bddf2ebf6f417", + "sha256:7f1ac7828857fcedb0361b48b9ac4821469f7694089d15550bbcf9ab22564a1d", + "sha256:87accdbba88f33efa7b592dc2e8b2a9c2cdbca73db2f9d5c510790428c09c154", + "sha256:8cee08f15d9e238ede42e9bbc1d6e7158d0ca4f176e4eab21f88ac819ae3bd7b", + "sha256:971ba928fcde01869361f504fcff3b7143b47d30de188b11c6357c0505824197", + "sha256:9c2e02f06c68092b875d5cbe4824238ab93a7fa35d9c38052c033f7ca45daa18", + "sha256:9c5a9da957c56e43d72126a3f5845603da00e0293720b03bde0aacffcf2dc04f", + "sha256:9df7126fd9db49e3a5a3999442cc67e9ee8971f3cb9644250107d7296cb2a164", + "sha256:b3edaec7e8b6dc5cd94523c6df4f294014df67097c8217a89929c99975811414", + "sha256:b535d35dea8bbb8195e7e2b40059e2253acb2b7579b73c1b432a35363694641d", + "sha256:bcf0724a62a5670e5718957e05c56ec2d6850267ea859f8ad2481838f889b42c", + "sha256:c00e7845d2f692ebfc7d5e4ec1a3fd87698e4337d09e58d6749a16aedfdf8612", + "sha256:c379e37b08c6c527181a397212346be39319fb64323741d23e46abd97a400d34", + "sha256:c5d1730b25d9a07727d20ad74bc1039bbbb0a6ca24e6769861c1aa5bf2c4c4a8", + "sha256:c5e73ba0d76eefc82ec0219d2301cb33bfe5205ed7a2602523111e2e56ccbd20", + "sha256:c697575d0e2b0a5f0433f679bda22f63873821d991e95a90e9e52aae517b2e32", + "sha256:cdeff998cb294896a34e5b2f00e383e7c5c4ef3b4bfa375d9104723f15186443", + "sha256:ceb5c832cc30663aeaf5e39657712f4c4241ad1f638d487ef7216258f6d41fe7", + "sha256:d34c0f6dbefd2e816e8f341d0df7d4763d382e3f452423e752ffd1e213da2512", + "sha256:db691fa174e8f7036afefe3061bc40ac2b770718be2862bfb03aabae09051aca", + "sha256:e7a903b5b45b0d9fa03ac6a331e1c1d6b7e0ab41c63b6217b3d10357b83c8b00", + "sha256:e7c08f57f75a2bb62d7ee80a89686a5e5669f199235c6d1dac75cd59374091c3", + "sha256:f42f23e152e4545157fa367b2435a1ace7571cab016ca26038867eb7df2c3631", + "sha256:fe2b3b4927d0bc03d02ad883f402d5de201dbc8894ac87d2e981e7d87430e60d" ], "index": "pypi", - "version": "==2.0.38" + "markers": "python_version >= '3.7'", + "version": "==2.0.43" }, "typing-extensions": { "hashes": [ - "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", - "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" + "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", + "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" ], "index": "pypi", - "version": "==4.12.2" + "markers": "python_version >= '3.9'", + "version": "==4.15.0" + }, + "tzdata": { + "hashes": [ + "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", + "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9" + ], + "markers": "python_version >= '2'", + "version": "==2025.2" }, "urllib3": { "hashes": [ - "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", - "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d" + "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", + "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc" ], "markers": "python_version >= '3.9'", - "version": "==2.3.0" + "version": "==2.5.0" }, "werkzeug": { "hashes": [ diff --git a/migrations/versions/0763d677d453_.py b/migrations/versions/0763d677d453_.py deleted file mode 100644 index 88964176f1..0000000000 --- a/migrations/versions/0763d677d453_.py +++ /dev/null @@ -1,35 +0,0 @@ -"""empty message - -Revision ID: 0763d677d453 -Revises: -Create Date: 2025-02-25 14:47:16.337069 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '0763d677d453' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('user', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('email', sa.String(length=120), nullable=False), - sa.Column('password', sa.String(), nullable=False), - sa.Column('is_active', sa.Boolean(), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('email') - ) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('user') - # ### end Alembic commands ### diff --git a/migrations/versions/21f5540bab2c_.py b/migrations/versions/21f5540bab2c_.py new file mode 100644 index 0000000000..e8f7e4cb82 --- /dev/null +++ b/migrations/versions/21f5540bab2c_.py @@ -0,0 +1,73 @@ +"""empty message + +Revision ID: 21f5540bab2c +Revises: +Create Date: 2025-09-20 16:17:04.733283 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '21f5540bab2c' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('users', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('email', sa.String(length=120), nullable=False), + sa.Column('password', sa.String(length=255), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('security_question', sa.String(length=255), nullable=True), + sa.Column('jpeg', sa.LargeBinary(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email') + ) + op.create_table('listings', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('booking_id', sa.Integer(), nullable=True), + sa.Column('airbnb_address', sa.String(length=255), nullable=False), + sa.Column('airbnb_zipcode', sa.String(length=15), nullable=True), + sa.ForeignKeyConstraint(['booking_id'], ['bookings.id'], name='fk_listings_current_booking', use_alter=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('bookings', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('google_calendar_id', sa.String(length=255), nullable=True), + sa.Column('listing_id', sa.Integer(), nullable=False), + sa.Column('airbnb_guest_first_name', sa.String(length=120), nullable=True), + sa.Column('airbnb_guest_last_name', sa.String(length=120), nullable=True), + sa.Column('airbnb_checkin', sa.Date(), nullable=True), + sa.Column('airbnb_checkout', sa.Date(), nullable=True), + sa.Column('airbnb_guestpic_url', sa.String(length=500), nullable=True), + sa.Column('airbnb_guestpic', sa.LargeBinary(), nullable=True), + sa.Column('reservation_url', sa.String(length=500), nullable=True), + sa.Column('phone_last4', sa.String(length=4), nullable=True), + sa.Column('needs_manual_details', sa.Boolean(), nullable=False), + sa.CheckConstraint('(airbnb_checkin IS NULL OR airbnb_checkout IS NULL) OR (airbnb_checkout >= airbnb_checkin)', name='ck_booking_checkout_after_checkin'), + sa.ForeignKeyConstraint(['listing_id'], ['listings.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('listing_id', 'google_calendar_id', name='uq_booking_listing_googleid') + ) + with op.batch_alter_table('bookings', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_bookings_google_calendar_id'), ['google_calendar_id'], unique=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('bookings', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_bookings_google_calendar_id')) + + op.drop_table('bookings') + op.drop_table('listings') + op.drop_table('users') + # ### end Alembic commands ### diff --git a/migrations/versions/ef02e5ef8fa8_.py b/migrations/versions/ef02e5ef8fa8_.py new file mode 100644 index 0000000000..f7cee9fcb1 --- /dev/null +++ b/migrations/versions/ef02e5ef8fa8_.py @@ -0,0 +1,132 @@ +"""empty message + +Revision ID: ef02e5ef8fa8 +Revises: 21f5540bab2c +Create Date: 2025-09-20 18:47:08.506715 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'ef02e5ef8fa8' +down_revision = '21f5540bab2c' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('bookings', schema=None) as batch_op: + batch_op.add_column(sa.Column('created_at', sa.DateTime(), nullable=False)) + batch_op.add_column(sa.Column('updated_at', sa.DateTime(), nullable=False)) + batch_op.alter_column('listing_id', + existing_type=sa.INTEGER(), + nullable=True) + batch_op.alter_column('airbnb_checkin', + existing_type=sa.DATE(), + type_=sa.DateTime(), + existing_nullable=True) + batch_op.alter_column('airbnb_checkout', + existing_type=sa.DATE(), + type_=sa.DateTime(), + existing_nullable=True) + batch_op.alter_column('reservation_url', + existing_type=sa.VARCHAR(length=500), + type_=sa.String(length=1024), + existing_nullable=True) + batch_op.alter_column('airbnb_guestpic_url', + existing_type=sa.VARCHAR(length=500), + type_=sa.String(length=1024), + existing_nullable=True) + batch_op.drop_constraint(batch_op.f('uq_booking_listing_googleid'), type_='unique') + batch_op.drop_constraint(batch_op.f('bookings_listing_id_fkey'), type_='foreignkey') + batch_op.drop_column('airbnb_guestpic') + batch_op.drop_column('phone_last4') + + with op.batch_alter_table('listings', schema=None) as batch_op: + batch_op.add_column(sa.Column('name', sa.String(length=255), nullable=True)) + batch_op.add_column(sa.Column('street', sa.String(length=255), nullable=True)) + batch_op.add_column(sa.Column('city', sa.String(length=120), nullable=True)) + batch_op.add_column(sa.Column('state', sa.String(length=80), nullable=True)) + batch_op.add_column(sa.Column('image_url', sa.String(length=1024), nullable=True)) + batch_op.add_column(sa.Column('created_at', sa.DateTime(), nullable=False)) + batch_op.add_column(sa.Column('updated_at', sa.DateTime(), nullable=False)) + batch_op.drop_constraint(batch_op.f('listings_user_id_fkey'), type_='foreignkey') + batch_op.drop_column('booking_id') + batch_op.drop_column('user_id') + batch_op.drop_column('airbnb_address') + batch_op.drop_column('airbnb_zipcode') + + with op.batch_alter_table('users', schema=None) as batch_op: + batch_op.add_column(sa.Column('created_at', sa.DateTime(), nullable=False)) + batch_op.add_column(sa.Column('updated_at', sa.DateTime(), nullable=False)) + batch_op.alter_column('email', + existing_type=sa.VARCHAR(length=120), + type_=sa.String(length=255), + existing_nullable=False) + batch_op.drop_constraint(batch_op.f('users_email_key'), type_='unique') + batch_op.create_index(batch_op.f('ix_users_email'), ['email'], unique=True) + batch_op.drop_column('security_question') + batch_op.drop_column('jpeg') + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('users', schema=None) as batch_op: + batch_op.add_column(sa.Column('jpeg', postgresql.BYTEA(), autoincrement=False, nullable=True)) + batch_op.add_column(sa.Column('security_question', sa.VARCHAR(length=255), autoincrement=False, nullable=True)) + batch_op.drop_index(batch_op.f('ix_users_email')) + batch_op.create_unique_constraint(batch_op.f('users_email_key'), ['email'], postgresql_nulls_not_distinct=False) + batch_op.alter_column('email', + existing_type=sa.String(length=255), + type_=sa.VARCHAR(length=120), + existing_nullable=False) + batch_op.drop_column('updated_at') + batch_op.drop_column('created_at') + + with op.batch_alter_table('listings', schema=None) as batch_op: + batch_op.add_column(sa.Column('airbnb_zipcode', sa.VARCHAR(length=15), autoincrement=False, nullable=True)) + batch_op.add_column(sa.Column('airbnb_address', sa.VARCHAR(length=255), autoincrement=False, nullable=False)) + batch_op.add_column(sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False)) + batch_op.add_column(sa.Column('booking_id', sa.INTEGER(), autoincrement=False, nullable=True)) + batch_op.create_foreign_key(batch_op.f('listings_user_id_fkey'), 'users', ['user_id'], ['id'], ondelete='CASCADE') + batch_op.drop_column('updated_at') + batch_op.drop_column('created_at') + batch_op.drop_column('image_url') + batch_op.drop_column('state') + batch_op.drop_column('city') + batch_op.drop_column('street') + batch_op.drop_column('name') + + with op.batch_alter_table('bookings', schema=None) as batch_op: + batch_op.add_column(sa.Column('phone_last4', sa.VARCHAR(length=4), autoincrement=False, nullable=True)) + batch_op.add_column(sa.Column('airbnb_guestpic', postgresql.BYTEA(), autoincrement=False, nullable=True)) + batch_op.create_foreign_key(batch_op.f('bookings_listing_id_fkey'), 'listings', ['listing_id'], ['id'], ondelete='CASCADE') + batch_op.create_unique_constraint(batch_op.f('uq_booking_listing_googleid'), ['listing_id', 'google_calendar_id'], postgresql_nulls_not_distinct=False) + batch_op.alter_column('airbnb_guestpic_url', + existing_type=sa.String(length=1024), + type_=sa.VARCHAR(length=500), + existing_nullable=True) + batch_op.alter_column('reservation_url', + existing_type=sa.String(length=1024), + type_=sa.VARCHAR(length=500), + existing_nullable=True) + batch_op.alter_column('airbnb_checkout', + existing_type=sa.DateTime(), + type_=sa.DATE(), + existing_nullable=True) + batch_op.alter_column('airbnb_checkin', + existing_type=sa.DateTime(), + type_=sa.DATE(), + existing_nullable=True) + batch_op.alter_column('listing_id', + existing_type=sa.INTEGER(), + nullable=False) + batch_op.drop_column('updated_at') + batch_op.drop_column('created_at') + + # ### end Alembic commands ### diff --git a/src/api/models.py b/src/api/models.py index da515f6a1a..4fbcc4a1d5 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -1,19 +1,119 @@ +from __future__ import annotations + +from datetime import datetime from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import String, Boolean -from sqlalchemy.orm import Mapped, mapped_column db = SQLAlchemy() + +# ---- User ------------------------------------------------------------------- +# Minimal User model so api/admin.py can `from .models import db, User` class User(db.Model): - id: Mapped[int] = mapped_column(primary_key=True) - email: Mapped[str] = mapped_column(String(120), unique=True, nullable=False) - password: Mapped[str] = mapped_column(nullable=False) - is_active: Mapped[bool] = mapped_column(Boolean(), nullable=False) + __tablename__ = "users" + + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(255), unique=True, nullable=False, index=True) + # store hashed password + password = db.Column(db.String(255), nullable=False) + is_active = db.Column(db.Boolean, nullable=False, default=True) + + created_at = db.Column(db.DateTime, nullable=False, + default=datetime.utcnow) + updated_at = db.Column( + db.DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow + ) + def __repr__(self) -> str: # pragma: no cover + return f"" - def serialize(self): + def serialize(self) -> dict: return { "id": self.id, "email": self.email, - # do not serialize the password, its a security breach - } \ No newline at end of file + "is_active": self.is_active, + } + + +# ---- Listing ---------------------------------------------------------------- +# Lightweight Listing model; safe to have even if you’re not using it yet. +class Listing(db.Model): + __tablename__ = "listings" + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(255), nullable=True) + street = db.Column(db.String(255), nullable=True) + city = db.Column(db.String(120), nullable=True) + state = db.Column(db.String(80), nullable=True) + image_url = db.Column(db.String(1024), nullable=True) + + created_at = db.Column(db.DateTime, nullable=False, + default=datetime.utcnow) + updated_at = db.Column( + db.DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow + ) + + def __repr__(self) -> str: # pragma: no cover + return f"" + + def serialize(self) -> dict: + return { + "id": self.id, + "name": self.name, + "street": self.street, + "city": self.city, + "state": self.state, + "image_url": self.image_url, + } + + +# ---- Booking ---------------------------------------------------------------- +# No phone/last4 anywhere. Dates & optional guest picture supported. +class Booking(db.Model): + __tablename__ = "bookings" + + id = db.Column(db.Integer, primary_key=True) + + # Google event UID + google_calendar_id = db.Column(db.String(255), index=True, nullable=True) + + # Optional association to a listing (kept simple: no FK constraint required) + listing_id = db.Column(db.Integer, nullable=True) + + # Optional guest names (manual) + airbnb_guest_first_name = db.Column(db.String(120), nullable=True) + airbnb_guest_last_name = db.Column(db.String(120), nullable=True) + + # Reservation dates + airbnb_checkin = db.Column(db.DateTime, nullable=True) + airbnb_checkout = db.Column(db.DateTime, nullable=True) + + # Reservation link (e.g., from calendar description) + reservation_url = db.Column(db.String(1024), nullable=True) + + # Optional image per booking (if you save one later) + airbnb_guestpic_url = db.Column(db.String(1024), nullable=True) + + # Bookkeeping + needs_manual_details = db.Column(db.Boolean, nullable=False, default=True) + created_at = db.Column(db.DateTime, nullable=False, + default=datetime.utcnow) + updated_at = db.Column( + db.DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow + ) + + def __repr__(self) -> str: # pragma: no cover + return f"" + + def serialize(self) -> dict: + return { + "id": self.id, + "google_calendar_id": self.google_calendar_id, + "listing_id": self.listing_id, + "airbnb_guest_first_name": self.airbnb_guest_first_name, + "airbnb_guest_last_name": self.airbnb_guest_last_name, + "airbnb_checkin": self.airbnb_checkin.isoformat() if self.airbnb_checkin else None, + "airbnb_checkout": self.airbnb_checkout.isoformat() if self.airbnb_checkout else None, + "reservation_url": self.reservation_url, + "airbnb_guestpic_url": self.airbnb_guestpic_url, + "needs_manual_details": self.needs_manual_details, + } diff --git a/src/api/routes.py b/src/api/routes.py index 029589a3a1..576a8d6d70 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -1,22 +1,171 @@ -""" -This module takes care of starting the API Server, Loading the DB and Adding the endpoints -""" -from flask import Flask, request, jsonify, url_for, Blueprint -from api.models import db, User -from api.utils import generate_sitemap, APIException -from flask_cors import CORS +from __future__ import annotations -api = Blueprint('api', __name__) +import os +import re +import pytz +import requests +from datetime import datetime, timedelta +from flask import Blueprint, current_app, jsonify, request +from icalendar import Calendar -# Allow CORS requests to this API -CORS(api) +api = Blueprint("api", __name__) -@api.route('/hello', methods=['POST', 'GET']) -def handle_hello(): +def _env(name: str, default: str | None = None) -> str | None: + return os.environ.get(name, default) - response_body = { - "message": "Hello! I'm a message that came from the backend, check the network tab on the google inspector and you will see the GET request" - } - return jsonify(response_body), 200 +RESERVATIONS_ICS_URL = _env("RESERVATIONS_ICS_URL") +DEFAULT_TZ = _env("DEFAULT_TIMEZONE", "America/New_York") + +# --- URL helpers ------------------------------------------------------------- + +RE_URL = re.compile(r"(https?://[^\s)]+)", re.I) + +# Accept common image URL patterns (with or without file extension) +RE_EXT_IMAGE = re.compile(r"\.(?:png|jpe?g|webp|gif)(?:\?.*)?$", re.I) + +# Google Drive link patterns +RE_DRIVE_FILE_VIEW = re.compile( + r"https?://drive\.google\.com/file/d/([^/]+)/view(?:\?[^ ]*)?", re.I) +RE_DRIVE_OPEN = re.compile( + r"https?://drive\.google\.com/open\?id=([^&]+)", re.I) +RE_DRIVE_UC = re.compile( + r"https?://drive\.google\.com/uc\?(?:export=\w+&)?id=([^&]+)", re.I) + + +def to_direct_image_url(url: str) -> str: + """ + Convert known providers (Google Drive) to a direct image URL. + If it already looks like an image or a direct-drive link, return as-is/converted. + """ + if not url: + return url + + # Google Drive conversions + m = RE_DRIVE_FILE_VIEW.search(url) or RE_DRIVE_OPEN.search( + url) or RE_DRIVE_UC.search(url) + if m: + file_id = m.group(1) + # Direct view endpoint that returns an image content-type + return f"https://drive.google.com/uc?export=view&id={file_id}" + + # Otherwise, if it looks like a normal image (by extension), return as is + if RE_EXT_IMAGE.search(url): + return url + + # Fallback: return original (may still work if server responds with image content-type) + return url + +# --- Datetime helpers -------------------------------------------------------- + + +def _to_tz(dt, tzname: str): + tz = pytz.timezone(tzname) + if isinstance(dt, datetime): + if dt.tzinfo is None: + return tz.localize(dt) + return dt.astimezone(tz) + return tz.localize(datetime(dt.year, dt.month, dt.day, 0, 0, 0)) + + +def _fix_all_day_checkout(start, end): + if isinstance(start, datetime) or isinstance(end, datetime): + return end + return end - timedelta(days=1) + +# --- Image extraction -------------------------------------------------------- + + +def _first_image_from_vevent(vevent) -> str | None: + """ + Extract an image URL from an event via: + 1) ATTACH property (may be one or many) + 2) Any URL in DESCRIPTION (first match) + For Google Drive links, convert to a direct-view URL. + """ + # 1) ATTACH + attach = vevent.get("attach") + if attach: + cands = attach if isinstance(attach, list) else [attach] + for a in cands: + url = to_direct_image_url(str(a)) + if url: + return url + + # 2) DESCRIPTION scan for any URL + desc = str(vevent.get("description") or "") + m = RE_URL.search(desc) + if m: + return to_direct_image_url(m.group(1)) + + return None + +# --- Core ICS parsing -------------------------------------------------------- + + +def _fetch_reserved_rows(tzname: str) -> list[dict]: + if not RESERVATIONS_ICS_URL: + raise RuntimeError("RESERVATIONS_ICS_URL is not configured") + + r = requests.get(RESERVATIONS_ICS_URL, timeout=20) + r.raise_for_status() + cal = Calendar.from_ical(r.content) + + rows: list[dict] = [] + for component in cal.walk(): + if component.name != "VEVENT": + continue + + summary = str(component.get("summary") or "") + # Only "Reserved ..." events; remove this if you want all events + if "reserved" not in summary.lower(): + continue + + uid = str(component.get("uid") or "").strip() + if not uid: + continue + + dtstart = component.get("dtstart") and component.get("dtstart").dt + dtend = component.get("dtend") and component.get("dtend").dt + if not dtstart or not dtend: + continue + + start_local = _to_tz(dtstart, tzname) + end_local = _to_tz(dtend, tzname) + + checkout_display = end_local + if not isinstance(dtstart, datetime) and not isinstance(dtend, datetime): + checkout_display = _to_tz( + _fix_all_day_checkout(dtstart, dtend), tzname) + + desc = str(component.get("description") or "") + m_url = RE_URL.search(desc) + reservation_url = m_url.group(1) if m_url else None + + image_url = _first_image_from_vevent(component) + + rows.append({ + "event": uid, # UID + "title": summary.strip(), # e.g., "Reserved - Andres" + "checkin": start_local.isoformat(), + "checkout": checkout_display.isoformat(), + "reservation_url": reservation_url, + "image": image_url, # now direct-view if it's a Drive link + }) + + rows.sort(key=lambda x: x["checkin"]) + return rows + +# --- Route ------------------------------------------------------------------- + + +@api.route("/calendar/reserved", methods=["GET"]) +def calendar_reserved(): + tzname = request.args.get("tz") or DEFAULT_TZ + try: + rows = _fetch_reserved_rows(tzname) + return jsonify(rows), 200 + except Exception as e: + current_app.logger.exception("calendar_reserved failed: %s", e) + return jsonify({"error": str(e)}), 500 diff --git a/src/app.py b/src/app.py index 1b3340c0fa..dcfef6059a 100644 --- a/src/app.py +++ b/src/app.py @@ -10,16 +10,19 @@ from api.routes import api from api.admin import setup_admin from api.commands import setup_commands - -# from models import Person +from flask_jwt_extended import JWTManager +from flask_cors import CORS # ⬅️ CORS for cross-origin requests ENV = "development" if os.getenv("FLASK_DEBUG") == "1" else "production" static_file_dir = os.path.join(os.path.dirname( os.path.realpath(__file__)), '../dist/') app = Flask(__name__) +# Change this "super secret" to something else! +app.config["JWT_SECRET_KEY"] = "super-secret" +jwt = JWTManager(app) app.url_map.strict_slashes = False -# database condiguration +# database configuration db_url = os.getenv("DATABASE_URL") if db_url is not None: app.config['SQLALCHEMY_DATABASE_URI'] = db_url.replace( @@ -31,13 +34,20 @@ MIGRATE = Migrate(app, db, compare_type=True) db.init_app(app) +# ---- CORS (allow frontend origin to call /api/*) ---------------------------- +origins_env = os.getenv("FRONTEND_ORIGINS", "") +origins_list = [o.strip() + for o in origins_env.split(",") if o.strip()] or ["*"] +CORS(app, resources={r"/api/*": {"origins": origins_list}}, + supports_credentials=True) + # add the admin setup_admin(app) -# add the admin +# add CLI commands setup_commands(app) -# Add all endpoints form the API with a "api" prefix +# Add all endpoints from the API with a "api" prefix app.register_blueprint(api, url_prefix='/api') # Handle/serialize errors like a JSON object @@ -57,6 +67,8 @@ def sitemap(): return send_from_directory(static_file_dir, 'index.html') # any other endpoint will try to serve it like a static file + + @app.route('/', methods=['GET']) def serve_any_other_file(path): if not os.path.isfile(os.path.join(static_file_dir, path)): diff --git a/src/front/assets/img/Andres.jpg b/src/front/assets/img/Andres.jpg new file mode 100644 index 0000000000..77a14a5c7c Binary files /dev/null and b/src/front/assets/img/Andres.jpg differ diff --git a/src/front/assets/img/Anouk.jpg b/src/front/assets/img/Anouk.jpg new file mode 100644 index 0000000000..3bf1d751f3 Binary files /dev/null and b/src/front/assets/img/Anouk.jpg differ diff --git a/src/front/assets/img/Caroline.jpg b/src/front/assets/img/Caroline.jpg new file mode 100644 index 0000000000..b1180538d3 Binary files /dev/null and b/src/front/assets/img/Caroline.jpg differ diff --git a/src/front/assets/img/Daniel.jpg b/src/front/assets/img/Daniel.jpg new file mode 100644 index 0000000000..96af3ed131 Binary files /dev/null and b/src/front/assets/img/Daniel.jpg differ diff --git a/src/front/assets/img/Diego.jpg b/src/front/assets/img/Diego.jpg new file mode 100644 index 0000000000..d30f564397 Binary files /dev/null and b/src/front/assets/img/Diego.jpg differ diff --git a/src/front/assets/img/Houcine.jpg b/src/front/assets/img/Houcine.jpg new file mode 100644 index 0000000000..ceedc51098 Binary files /dev/null and b/src/front/assets/img/Houcine.jpg differ diff --git a/src/front/assets/img/Jessica.jpg b/src/front/assets/img/Jessica.jpg new file mode 100644 index 0000000000..87796e26b2 Binary files /dev/null and b/src/front/assets/img/Jessica.jpg differ diff --git a/src/front/assets/img/Jianyan.jpg b/src/front/assets/img/Jianyan.jpg new file mode 100644 index 0000000000..a855b42061 Binary files /dev/null and b/src/front/assets/img/Jianyan.jpg differ diff --git a/src/front/assets/img/Ksana.jpg b/src/front/assets/img/Ksana.jpg new file mode 100644 index 0000000000..53666ea082 Binary files /dev/null and b/src/front/assets/img/Ksana.jpg differ diff --git a/src/front/assets/img/Luis.jpg b/src/front/assets/img/Luis.jpg new file mode 100644 index 0000000000..585fcb0fca Binary files /dev/null and b/src/front/assets/img/Luis.jpg differ diff --git a/src/front/assets/img/Nykealah.jpg b/src/front/assets/img/Nykealah.jpg new file mode 100644 index 0000000000..20f08c8930 Binary files /dev/null and b/src/front/assets/img/Nykealah.jpg differ diff --git a/src/front/assets/img/Oluyinka.jpg b/src/front/assets/img/Oluyinka.jpg new file mode 100644 index 0000000000..1a01cdd583 Binary files /dev/null and b/src/front/assets/img/Oluyinka.jpg differ diff --git a/src/front/assets/img/Rebeca.jpg b/src/front/assets/img/Rebeca.jpg new file mode 100644 index 0000000000..086a56c4b8 Binary files /dev/null and b/src/front/assets/img/Rebeca.jpg differ diff --git a/src/front/assets/img/Sievers.jpg b/src/front/assets/img/Sievers.jpg new file mode 100644 index 0000000000..32f5fa304d Binary files /dev/null and b/src/front/assets/img/Sievers.jpg differ diff --git a/src/front/components/Footer.jsx b/src/front/components/Footer.jsx index f06302dbd2..3f16fdafc4 100644 --- a/src/front/components/Footer.jsx +++ b/src/front/components/Footer.jsx @@ -1,11 +1,15 @@ -export const Footer = () => ( +import React from "react"; + +export const Footer = () => { + const year = new Date().getFullYear(); + return ( -); + ); +}; diff --git a/src/front/components/Navbar.jsx b/src/front/components/Navbar.jsx index 30d43a2636..0d28f28dff 100644 --- a/src/front/components/Navbar.jsx +++ b/src/front/components/Navbar.jsx @@ -1,19 +1,24 @@ import { Link } from "react-router-dom"; export const Navbar = () => { - return ( ); -}; \ No newline at end of file +}; diff --git a/src/front/components/NearbyRestaurants.jsx b/src/front/components/NearbyRestaurants.jsx new file mode 100644 index 0000000000..4aee1ec926 --- /dev/null +++ b/src/front/components/NearbyRestaurants.jsx @@ -0,0 +1,124 @@ +import React, { useState, useEffect } from 'react'; +import { useGeoLocation } from '../hooks/GeoLocation'; + +export const NearbyRestaurants = () => { + const location = useGeoLocation(); // Your hook returns { latitude, longitude, error } + const [restaurants, setRestaurants] = useState([]); + const [loading, setLoading] = useState(false); + const [apiError, setApiError] = useState(null); + + useEffect(() => { + // Only fetch when we have valid coordinates + if (location.latitude && location.longitude && !location.error) { + fetchNearbyRestaurants(); + } + }, [location.latitude, location.longitude]); + + const fetchNearbyRestaurants = async () => { + setLoading(true); + setApiError(null); + + try { + const response = await fetch( + `${import.meta.env.VITE_BACKEND_URL}api/restaurants/nearby?latitude=${location.latitude}&longitude=${location.longitude}&radius=5000` + ); + + if (response.ok) { + const data = await response.json(); + setRestaurants(data.businesses || []); + } else { + setApiError('Failed to fetch restaurants'); + } + } catch (err) { + setApiError('Error fetching restaurants: ' + err.message); + } finally { + setLoading(false); + } + }; + + // Show loading while getting location + if (!location.latitude && !location.error) { + return ( +
+
+ Getting your location... +
+

Getting your location...

+
+ ); + } + + // Show geolocation error + if (location.error) { + return ( +
+ Location Error: {location.error} +
+ ); + } + + return ( +
+

Nearby Restaurants

+ +

+ Showing restaurants near: {location.latitude.toFixed(4)}, {location.longitude.toFixed(4)} +

+ + {loading && ( +
+
+ Loading restaurants... +
+
+ )} + + {apiError && ( +
+ {apiError} +
+ )} + +
+ {restaurants.map((restaurant) => ( +
+
+ {restaurant.image_url && ( + {restaurant.name} + )} +
+
{restaurant.name}
+

+ + {restaurant.categories?.map(cat => cat.title).join(', ')} + +

+

+ + ⭐ {restaurant.rating} + + + {(restaurant.distance / 1000).toFixed(1)}km away + +

+

+ {restaurant.location?.display_address?.join(', ')} +

+ {restaurant.phone && ( + + Call + + )} +
+
+
+ ))} +
+
+ ); +}; \ No newline at end of file diff --git a/src/front/hooks/GeoLocation.jsx b/src/front/hooks/GeoLocation.jsx new file mode 100644 index 0000000000..8665084502 --- /dev/null +++ b/src/front/hooks/GeoLocation.jsx @@ -0,0 +1,30 @@ +import React, { useState, useEffect } from "react"; + +export const useGeoLocation = () => { + const [location, setLocation] = useState({ latitude: null, longitude: null, error: null }); + + useEffect(() => { + if (!navigator.geolocation) { + setLocation(loc => ({ ...loc, error: "Geolocation is not supported by your browser" })); + } else { + navigator.geolocation.getCurrentPosition( + (position) => { + setLocation(loc => ({ + ...loc, + latitude: position.coords.latitude, + longitude: position.coords.longitude, + error: null + })); + console.log("Latitude:", position.coords.latitude); + console.log("Longitude:", position.coords.longitude); + }, + (error) => { + setLocation(loc => ({ ...loc, error: error.message })); + console.error("Error Code = " + error.code + " - " + error.message); + } + ); + } + }, []); + + return location; +}; \ No newline at end of file diff --git a/src/front/pages/Account.jsx b/src/front/pages/Account.jsx new file mode 100644 index 0000000000..edfbab34c9 --- /dev/null +++ b/src/front/pages/Account.jsx @@ -0,0 +1,297 @@ +import React, { useEffect, useMemo, useRef, useState } from "react"; +import useGlobalReducer from "../hooks/useGlobalReducer.jsx"; + +const API_BASE = import.meta.env.VITE_API_BASE_URL || "/api"; + +/* -------------------- EXPLICIT PHOTO IMPORTS -------------------- */ +import AndresPhoto from "../assets/img/Andres.jpg"; +import AnoukPhoto from "../assets/img/Anouk.jpg"; +import CarolinePhoto from "../assets/img/Caroline.jpg"; +import DanielPhoto from "../assets/img/Daniel.jpg"; +import DiegoPhoto from "../assets/img/Diego.jpg"; +import HoucinePhoto from "../assets/img/Houcine.jpg"; +import JessicaPhoto from "../assets/img/Jessica.jpg"; +import JianyanPhoto from "../assets/img/Jianyan.jpg"; +import KsanaPhoto from "../assets/img/Ksana.jpg"; +import LuisPhoto from "../assets/img/Luis.jpg"; +import NykealahPhoto from "../assets/img/Nykealah.jpg"; +import OluyinkaPhoto from "../assets/img/Oluyinka.jpg"; +import RebecaPhoto from "../assets/img/Rebeca.jpg"; +import SieversPhoto from "../assets/img/Sievers.jpg"; + +const IMAGES = { + andres: AndresPhoto, + anouk: AnoukPhoto, + caroline: CarolinePhoto, + daniel: DanielPhoto, + diego: DiegoPhoto, + houcine: HoucinePhoto, + jessica: JessicaPhoto, + jianyan: JianyanPhoto, + ksana: KsanaPhoto, + luis: LuisPhoto, + nykealah: NykealahPhoto, + oluyinka: OluyinkaPhoto, + rebeca: RebecaPhoto, + sievers: SieversPhoto, +}; + +/* -------------------- HELPERS -------------------- */ + +function extractGuestName(eventTitle) { + if (!eventTitle) return ""; + const cleaned = String(eventTitle).trim(); + const patterns = [/^Reserved\s*[-–—]\s*/i, /^Reserva(do|da)?\s*[-–—]\s*/i]; + let name = cleaned; + for (const p of patterns) name = name.replace(p, ""); + return name.trim(); +} + +function normalizeKey(s) { + return String(s || "") + .toLowerCase() + .normalize("NFKD") + .replace(/[\u0300-\u036f]/g, "") + .replace(/[^a-z0-9]/g, ""); +} + +function fmtDate(iso) { + if (!iso) return ""; + try { + const d = new Date(iso); + return d.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" }); + } catch { + return iso; + } +} + +function toDriveDirect(url) { + if (!url) return null; + const m1 = url.match(/https?:\/\/drive\.google\.com\/file\/d\/([^/]+)\/view/i); + if (m1) return `https://drive.google.com/uc?export=view&id=${m1[1]}`; + const m2 = url.match(/https?:\/\/drive\.google\.com\/open\?id=([^&]+)/i); + if (m2) return `https://drive.google.com/uc?export=view&id=${m2[1]}`; + const m3 = url.match(/https?:\/\/drive\.google\.com\/uc\?(?:export=\w+&)?id=([^&]+)/i); + if (m3) return `https://drive.google.com/uc?export=view&id=${m3[1]}`; + return null; +} + +const CARD_HEIGHT = 180; // unified height for image + content + +export const Account = () => { + const { store } = + (typeof useGlobalReducer === "function" ? useGlobalReducer() : { store: {} }) || { store: {} }; + + const token = store?.token ?? true; + const email = store?.user?.email ?? "Guest"; + + const [reservations, setReservations] = useState([]); + const [loading, setLoading] = useState(true); + const [err, setErr] = useState(null); + + useEffect(() => { + const url = `${API_BASE}/calendar/reserved?tz=America/New_York`; + fetch(url) + .then(async (r) => { + const txt = await r.text(); + if (!r.ok) throw new Error(`HTTP ${r.status}: ${txt.slice(0, 200)}`); + try { + return JSON.parse(txt); + } catch { + throw new Error(`Expected JSON, got: ${txt.slice(0, 120)}`); + } + }) + .then((data) => { + setReservations(Array.isArray(data) ? data : []); + setLoading(false); + }) + .catch((e) => { + setErr(e.message); + setLoading(false); + }); + }, []); + + const items = useMemo( + () => + reservations.map((r) => { + const title = r.title || r.event; + const guestName = extractGuestName(title); + const keyName = normalizeKey(guestName); + + const imported = IMAGES[keyName]; + const backendImg = (r.image && r.image.trim()) || null; + const directFromReservation = toDriveDirect(r.reservation_url || ""); + const imageSrc = + imported || + backendImg || + (directFromReservation && directFromReservation.trim()) || + "https://picsum.photos/seed/guest/600/400"; + + const checkinDate = r.checkin ? new Date(r.checkin) : null; + const checkoutDate = r.checkout ? new Date(r.checkout) : null; + + return { + key: r.event || crypto.randomUUID(), + guestName, + checkinText: fmtDate(r.checkin), + checkoutText: fmtDate(r.checkout), + checkinDate, + checkoutDate, + image: imageSrc, + }; + }), + [reservations] + ); + + // Helpers for date window checks + const todayStart = useMemo(() => { + const d = new Date(); + d.setHours(0, 0, 0, 0); + return d; + }, []); + const todayEnd = useMemo(() => { + const d = new Date(); + d.setHours(23, 59, 59, 999); + return d; + }, []); + + const isCurrent = (ci, co) => + ci && co && todayStart <= co && todayEnd >= ci; // inclusive window + const isPast = (co) => co && co < todayStart; + + // Find current/next card index for initial scroll + const currentIndex = useMemo(() => { + if (!items.length) return 0; + const cur = items.findIndex((it) => isCurrent(it.checkinDate, it.checkoutDate)); + if (cur !== -1) return cur; + const upcoming = items.findIndex((it) => it.checkinDate && it.checkinDate > todayEnd); + return upcoming !== -1 ? upcoming : 0; + }, [items, todayEnd, todayStart]); + + const listRef = useRef(null); + const cardRefs = useRef([]); + + useEffect(() => { + if (!listRef.current || !cardRefs.current[currentIndex]) return; + cardRefs.current[currentIndex].scrollIntoView({ block: "start" }); + }, [currentIndex, items.length]); + + if (!token) { + return ( +
+
+

Not authorized

+
+
+ ); + } + + return ( + // Lock outer page scroll; only the list scrolls +
+
+ {/* Top-right logo */} +
+ + WhiteGlove BnB + +
+ +

Welcome {email}

+ +
+

Here are your upcoming reservations...

+ {loading && Loading…} + {err && Error: {err}} +
+ + {/* Scrollable list — with top gap + scroll padding to prevent overlap */} +
+ {items.map((it, idx) => { + const shaded = isPast(it.checkoutDate); // old reservations look lighter + const showCurrent = isCurrent(it.checkinDate, it.checkoutDate); // badge only when hosting now + + return ( +
(cardRefs.current[idx] = el)} + className="col-12 d-flex justify-content-center" + style={{ scrollMarginTop: 16 }} + > +
+ {/* Badge: top-right inside the card */} + {showCurrent && ( + + Currently hosting + + )} + +
+ {/* Left image pane */} +
+ Guest +
+ + {/* Right content pane */} +
+
+
+ {it.guestName || "(No name in event)"} +
+ +
+ Check-in: {it.checkinText || "—"} +
+
+ Check-out: {it.checkoutText || "—"} +
+ + +
+
+
+
+
+ ); + })} + + {!loading && items.length === 0 && !err && ( +
+
+ No reservations found. +
+
+ )} +
+
+
+ ); +}; + +export default Account; \ No newline at end of file diff --git a/src/front/pages/Forgot-password.jsx b/src/front/pages/Forgot-password.jsx new file mode 100644 index 0000000000..286fb66695 --- /dev/null +++ b/src/front/pages/Forgot-password.jsx @@ -0,0 +1,108 @@ +import React, { useState } from "react"; +import { Link } from "react-router-dom"; + +const API_BASE_URL = "https://glorious-space-halibut-r49v46gv46qfx5qw-3001.app.github.dev"; + +export const ForgotPassword = () => { + const [email, setEmail] = useState(""); + const [favoritePet, setFavoritePet] = useState(""); + const [busy, setBusy] = useState(false); + const [resultText, setResultText] = useState(""); // shows password or error + + const handleSubmit = async (e) => { + e.preventDefault(); + setResultText(""); + setBusy(true); + + try { + const res = await fetch(`${API_BASE_URL}/api/forgot-password`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + email: email.trim(), + favorite_pet: favoritePet.trim(), + }), + }); + + if (res.ok) { + const data = await res.json(); + // Reveal password on success (as you specified for this project) + setResultText(data?.password ?? ""); + } else { + setResultText("The information entered is incorrect, please contact the administator"); + } + } catch { + setResultText("The information entered is incorrect, please contact the administator"); + } finally { + setBusy(false); + } + }; + + return ( +
+
+
+
+
+

Forgot Password

+

+ Enter your Email and Favorite Pet then submit to get your password. +

+ +
+
+ + setEmail(e.target.value)} + autoComplete="email" + required + placeholder="you@example.com" + /> +
+ +
+ + setFavoritePet(e.target.value)} + autoComplete="off" + required + placeholder="e.g., Luna" + /> +
+ +
+ +
+
+ + {/* Box directly under the submit button */} +
+ {resultText} +
+ +
+ Return to sign-in page +
+
+
+
+
+
+ ); +}; diff --git a/src/front/pages/Home.jsx b/src/front/pages/Home.jsx index 341ed21768..c431d2826c 100644 --- a/src/front/pages/Home.jsx +++ b/src/front/pages/Home.jsx @@ -1,52 +1,57 @@ -import React, { useEffect } from "react" -import rigoImageUrl from "../assets/img/rigo-baby.jpg"; -import useGlobalReducer from "../hooks/useGlobalReducer.jsx"; +// src/front/js/pages/Home.jsx +import React from "react"; +import { Link } from "react-router-dom"; export const Home = () => { - - const { store, dispatch } = useGlobalReducer() - - const loadMessage = async () => { - try { - const backendUrl = import.meta.env.VITE_BACKEND_URL - - if (!backendUrl) throw new Error("VITE_BACKEND_URL is not defined in .env file") - - const response = await fetch(backendUrl + "/api/hello") - const data = await response.json() - - if (response.ok) dispatch({ type: "set_hello", payload: data.message }) - - return data - - } catch (error) { - if (error.message) throw new Error( - `Could not fetch the message from the backend. - Please check if the backend is running and the backend port is public.` - ); - } - - } - - useEffect(() => { - loadMessage() - }, []) - - return ( -
-

Hello Rigo!!

-

- Rigo Baby -

-
- {store.message ? ( - {store.message} - ) : ( - - Loading message from the backend (make sure your python 🐍 backend is running)... - - )} -
-
- ); -}; \ No newline at end of file + const year = new Date().getFullYear(); + + return ( +
+ +
+
+
+