From 4d323b4de5ef0fc6a57582e395bf447612115600 Mon Sep 17 00:00:00 2001 From: Oscar Parra <oscarparrapalomino@gmail.com> Date: Fri, 21 Feb 2025 13:56:50 +0000 Subject: [PATCH 01/25] First Commit --- Pipfile.lock | 654 +++++++++++++++------------ migrations/README | 1 + migrations/alembic.ini | 50 ++ migrations/env.py | 113 +++++ migrations/script.py.mako | 24 + migrations/versions/22b5bf5541c4_.py | 35 ++ src/front/img/microphone.jpg | Bin 0 -> 112253 bytes src/front/js/layout.js | 53 ++- src/front/js/pages/login.js | 76 ++++ src/front/styles/home.css | 10 +- src/front/styles/login.css | 7 + 11 files changed, 707 insertions(+), 316 deletions(-) create mode 100644 migrations/README create mode 100644 migrations/alembic.ini create mode 100644 migrations/env.py create mode 100644 migrations/script.py.mako create mode 100644 migrations/versions/22b5bf5541c4_.py create mode 100644 src/front/img/microphone.jpg create mode 100644 src/front/js/pages/login.js create mode 100644 src/front/styles/login.css diff --git a/Pipfile.lock b/Pipfile.lock index a391864e9d..508225b0a1 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "74f92d76f687bb774828613a3a513123fe2ffdb429b95b351d29721dddfd3fb8" + "sha256": "4f0e9a772f04b621ff0313b7ecfa468af1526aa27df8bfcacac6955d499d352d" }, "pipfile-spec": 6, "requires": { @@ -18,73 +18,91 @@ "default": { "alembic": { "hashes": [ - "sha256:6880dec4f28dd7bd999d2ed13fbe7c9d4337700a44d11a524c0ce0c59aaf0dbd", - "sha256:e8a6ff9f3b1887e1fed68bfb8fb9a000d8f61c21bdcc85b67bb9f87fcbc4fce3" + "sha256:1acdd7a3a478e208b0503cd73614d5e4c6efafa4e73518bb60e4f2846a37b1c5", + "sha256:496e888245a53adf1498fcab31713a469c65836f8de76e01399aa1c3e90dd213" ], - "markers": "python_version >= '3.7'", - "version": "==1.9.2" + "markers": "python_version >= '3.8'", + "version": "==1.14.1" + }, + "blinker": { + "hashes": [ + "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", + "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc" + ], + "markers": "python_version >= '3.9'", + "version": "==1.9.0" }, "certifi": { "hashes": [ - "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", - "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" + "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", + "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe" ], "markers": "python_version >= '3.6'", - "version": "==2022.12.7" + "version": "==2025.1.31" }, "click": { "hashes": [ - "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", - "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" + "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", + "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a" ], "markers": "python_version >= '3.7'", - "version": "==8.1.3" + "version": "==8.1.8" }, "cloudinary": { "hashes": [ - "sha256:f52a1f5eb2c6820f13aa01c109caa5937ad3fd6caf5967817d0ef6c113403afc" + "sha256:ba223705409b2aaddd5196c2184d65f50a83dffcba3b94f3727658ff6a0172a3", + "sha256:e4191b470c5bae55542b64e0a78659af42971880294456dca480bc974fa9280a" ], "index": "pypi", - "version": "==1.31.0" + "version": "==1.42.2" }, "flask": { "hashes": [ - "sha256:642c450d19c4ad482f96729bd2a8f6d32554aa1e231f4f6b4e7e5264b16cca2b", - "sha256:b9c46cc36662a7949f34b52d8ec7bb59c0d74ba08ba6cb9ce9adc1d8676d9526" + "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac", + "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136" ], "index": "pypi", - "version": "==2.2.2" + "version": "==3.1.0" }, "flask-admin": { "hashes": [ - "sha256:424ffc79b7b0dfff051555686ea12e86e48dffacac14beaa319fb4502ac40988" + "sha256:24cae2af832b6a611a01d7dc35f42d266c1d6c75a426b869d8cb241b78233369", + "sha256:fd8190f1ec3355913a22739c46ed3623f1d82b8112cde324c60a6fc9b21c9406" ], "index": "pypi", - "version": "==1.6.0" + "version": "==1.6.1" }, "flask-cors": { "hashes": [ - "sha256:74efc975af1194fc7891ff5cd85b0f7478be4f7f59fe158102e91abb72bb4438", - "sha256:b60839393f3b84a0f3746f6cdca56c1ad7426aa738b70d6c61375857823181de" + "sha256:5aadb4b950c4e93745034594d9f3ea6591f734bb3662e16e255ffbf5e89c88ef", + "sha256:b9e307d082a9261c100d8fb0ba909eec6a228ed1b60a8315fd85f783d61910bc" + ], + "index": "pypi", + "version": "==5.0.0" + }, + "flask-jwt-extended": { + "hashes": [ + "sha256:63a28fc9731bcc6c4b8815b6f954b5904caa534fc2ae9b93b1d3ef12930dca95", + "sha256:9215d05a9413d3855764bcd67035e75819d23af2fafb6b55197eb5a3313fdfb2" ], "index": "pypi", - "version": "==3.0.10" + "version": "==4.6.0" }, "flask-migrate": { "hashes": [ - "sha256:8662a9dd391ce36deeaf3265987319c20fdb4c8a45306a32ba4f8224459abed4", - "sha256:a0062c8d3f32de02847086b46cfc389412f78c71c89a619ebd7097e89d72ea4b" + "sha256:1a336b06eb2c3ace005f5f2ded8641d534c18798d64061f6ff11f79e1434126d", + "sha256:24d8051af161782e0743af1b04a152d007bad9772b2bca67b7ec1e8ceeb3910d" ], "index": "pypi", - "version": "==4.0.3" + "version": "==4.1.0" }, "flask-sqlalchemy": { "hashes": [ - "sha256:2764335f3c9d7ebdc9ed6044afaf98aae9fa50d7a074cef55dde307ec95903ec", - "sha256:add5750b2f9cd10512995261ee2aa23fab85bd5626061aa3c564b33bb4aa780a" + "sha256:c5765e58ca145401b52106c0f46178569243c5da25556be2c231ecc60867c5b1", + "sha256:cabb6600ddd819a9f859f36515bb1bd8e7dbf30206cc679d2b081dff9e383283" ], "index": "pypi", - "version": "==3.0.3" + "version": "==3.0.5" }, "flask-swagger": { "hashes": [ @@ -96,304 +114,346 @@ }, "greenlet": { "hashes": [ - "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a", - "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a", - "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43", - "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33", - "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8", - "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088", - "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca", - "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343", - "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645", - "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db", - "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df", - "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3", - "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86", - "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2", - "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a", - "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf", - "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7", - "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394", - "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40", - "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3", - "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6", - "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74", - "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0", - "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3", - "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91", - "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5", - "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9", - "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8", - "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b", - "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6", - "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb", - "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73", - "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b", - "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df", - "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9", - "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f", - "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0", - "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857", - "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a", - "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249", - "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30", - "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292", - "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b", - "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d", - "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b", - "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c", - "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca", - "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7", - "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75", - "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae", - "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b", - "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470", - "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564", - "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9", - "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099", - "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0", - "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5", - "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19", - "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1", - "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526" + "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' 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": "==2.0.2" + "version": "==3.1.1" }, "gunicorn": { "hashes": [ - "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e", - "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8" + "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", + "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec" ], "index": "pypi", - "version": "==20.1.0" + "version": "==23.0.0" }, "itsdangerous": { "hashes": [ - "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44", - "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a" + "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", + "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173" ], - "markers": "python_version >= '3.7'", - "version": "==2.1.2" + "markers": "python_version >= '3.8'", + "version": "==2.2.0" }, "jinja2": { "hashes": [ - "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", - "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" + "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", + "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb" ], "markers": "python_version >= '3.7'", - "version": "==3.1.2" + "version": "==3.1.5" }, "mako": { "hashes": [ - "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818", - "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34" + "sha256:95920acccb578427a9aa38e37a186b1e43156c87260d7ba18ca63aa4c7cbd3a1", + "sha256:b5d65ff3462870feec922dbccf38f6efb44e5714d7b593a656be86663d8600ac" ], - "markers": "python_version >= '3.7'", - "version": "==1.2.4" + "markers": "python_version >= '3.8'", + "version": "==1.3.9" }, "markupsafe": { "hashes": [ - "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed", - "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc", - "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2", - "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460", - "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7", - "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0", - "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1", - "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa", - "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03", - "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323", - "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65", - "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013", - "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036", - "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f", - "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4", - "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419", - "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2", - "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619", - "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a", - "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a", - "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd", - "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7", - "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666", - "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65", - "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859", - "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625", - "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff", - "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156", - "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd", - "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba", - "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f", - "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1", - "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094", - "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a", - "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513", - "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed", - "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d", - "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3", - "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147", - "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c", - "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603", - "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601", - "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a", - "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1", - "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d", - "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3", - "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54", - "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2", - "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6", - "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58" + "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", + "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", + "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", + "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", + "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", + "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", + "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", + "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", + "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", + "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", + "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", + "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", + "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", + "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", + "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", + "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", + "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", + "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", + "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", + "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", + "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", + "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", + "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", + "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", + "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", + "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", + "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", + "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", + "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", + "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", + "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", + "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", + "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", + "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", + "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", + "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", + "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", + "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", + "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", + "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", + "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", + "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", + "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", + "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", + "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", + "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", + "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", + "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", + "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", + "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", + "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", + "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", + "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", + "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", + "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", + "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", + "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", + "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", + "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", + "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", + "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50" ], - "markers": "python_version >= '3.7'", - "version": "==2.1.2" + "markers": "python_version >= '3.9'", + "version": "==3.0.2" + }, + "packaging": { + "hashes": [ + "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", + "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" + ], + "markers": "python_version >= '3.8'", + "version": "==24.2" }, "psycopg2-binary": { "hashes": [ - "sha256:00475004e5ed3e3bf5e056d66e5dcdf41a0dc62efcd57997acd9135c40a08a50", - "sha256:01ad49d68dd8c5362e4bfb4158f2896dc6e0c02e87b8a3770fc003459f1a4425", - "sha256:024030b13bdcbd53d8a93891a2cf07719715724fc9fee40243f3bd78b4264b8f", - "sha256:02551647542f2bf89073d129c73c05a25c372fc0a49aa50e0de65c3c143d8bd0", - "sha256:043a9fd45a03858ff72364b4b75090679bd875ee44df9c0613dc862ca6b98460", - "sha256:05b3d479425e047c848b9782cd7aac9c6727ce23181eb9647baf64ffdfc3da41", - "sha256:0775d6252ccb22b15da3b5d7adbbf8cfe284916b14b6dc0ff503a23edb01ee85", - "sha256:1764546ffeaed4f9428707be61d68972eb5ede81239b46a45843e0071104d0dd", - "sha256:1e491e6489a6cb1d079df8eaa15957c277fdedb102b6a68cfbf40c4994412fd0", - "sha256:212757ffcecb3e1a5338d4e6761bf9c04f750e7d027117e74aa3cd8a75bb6fbd", - "sha256:215d6bf7e66732a514f47614f828d8c0aaac9a648c46a831955cb103473c7147", - "sha256:25382c7d174c679ce6927c16b6fbb68b10e56ee44b1acb40671e02d29f2fce7c", - "sha256:2abccab84d057723d2ca8f99ff7b619285d40da6814d50366f61f0fc385c3903", - "sha256:2d964eb24c8b021623df1c93c626671420c6efadbdb8655cb2bd5e0c6fa422ba", - "sha256:2ec46ed947801652c9643e0b1dc334cfb2781232e375ba97312c2fc256597632", - "sha256:2ef892cabdccefe577088a79580301f09f2a713eb239f4f9f62b2b29cafb0577", - "sha256:33e632d0885b95a8b97165899006c40e9ecdc634a529dca7b991eb7de4ece41c", - "sha256:3520d7af1ebc838cc6084a3281145d5cd5bdd43fdef139e6db5af01b92596cb7", - "sha256:3d790f84201c3698d1bfb404c917f36e40531577a6dda02e45ba29b64d539867", - "sha256:3fc33295cfccad697a97a76dec3f1e94ad848b7b163c3228c1636977966b51e2", - "sha256:422e3d43b47ac20141bc84b3d342eead8d8099a62881a501e97d15f6addabfe9", - "sha256:426c2ae999135d64e6a18849a7d1ad0e1bd007277e4a8f4752eaa40a96b550ff", - "sha256:46512486be6fbceef51d7660dec017394ba3e170299d1dc30928cbedebbf103a", - "sha256:46850a640df62ae940e34a163f72e26aca1f88e2da79148e1862faaac985c302", - "sha256:484405b883630f3e74ed32041a87456c5e0e63a8e3429aa93e8714c366d62bd1", - "sha256:4e7904d1920c0c89105c0517dc7e3f5c20fb4e56ba9cdef13048db76947f1d79", - "sha256:56b2957a145f816726b109ee3d4e6822c23f919a7d91af5a94593723ed667835", - "sha256:5c6527c8efa5226a9e787507652dd5ba97b62d29b53c371a85cd13f957fe4d42", - "sha256:5cbc554ba47ecca8cd3396ddaca85e1ecfe3e48dd57dc5e415e59551affe568e", - "sha256:5d28ecdf191db558d0c07d0f16524ee9d67896edf2b7990eea800abeb23ebd61", - "sha256:5fc447058d083b8c6ac076fc26b446d44f0145308465d745fba93a28c14c9e32", - "sha256:63e318dbe52709ed10d516a356f22a635e07a2e34c68145484ed96a19b0c4c68", - "sha256:68d81a2fe184030aa0c5c11e518292e15d342a667184d91e30644c9d533e53e1", - "sha256:6e63814ec71db9bdb42905c925639f319c80e7909fb76c3b84edc79dadef8d60", - "sha256:6f8a9bcab7b6db2e3dbf65b214dfc795b4c6b3bb3af922901b6a67f7cb47d5f8", - "sha256:70831e03bd53702c941da1a1ad36c17d825a24fbb26857b40913d58df82ec18b", - "sha256:74eddec4537ab1f701a1647214734bc52cee2794df748f6ae5908e00771f180a", - "sha256:7b3751857da3e224f5629400736a7b11e940b5da5f95fa631d86219a1beaafec", - "sha256:7cf1d44e710ca3a9ce952bda2855830fe9f9017ed6259e01fcd71ea6287565f5", - "sha256:7d07f552d1e412f4b4e64ce386d4c777a41da3b33f7098b6219012ba534fb2c2", - "sha256:7d88db096fa19d94f433420eaaf9f3c45382da2dd014b93e4bf3215639047c16", - "sha256:7ee3095d02d6f38bd7d9a5358fcc9ea78fcdb7176921528dd709cc63f40184f5", - "sha256:902844f9c4fb19b17dfa84d9e2ca053d4a4ba265723d62ea5c9c26b38e0aa1e6", - "sha256:937880290775033a743f4836aa253087b85e62784b63fd099ee725d567a48aa1", - "sha256:95076399ec3b27a8f7fa1cc9a83417b1c920d55cf7a97f718a94efbb96c7f503", - "sha256:9c38d3869238e9d3409239bc05bc27d6b7c99c2a460ea337d2814b35fb4fea1b", - "sha256:9e32cedc389bcb76d9f24ea8a012b3cb8385ee362ea437e1d012ffaed106c17d", - "sha256:9ffdc51001136b699f9563b1c74cc1f8c07f66ef7219beb6417a4c8aaa896c28", - "sha256:a0adef094c49f242122bb145c3c8af442070dc0e4312db17e49058c1702606d4", - "sha256:a36a0e791805aa136e9cbd0ffa040d09adec8610453ee8a753f23481a0057af5", - "sha256:a7e518a0911c50f60313cb9e74a169a65b5d293770db4770ebf004245f24b5c5", - "sha256:af0516e1711995cb08dc19bbd05bec7dbdebf4185f68870595156718d237df3e", - "sha256:b8104f709590fff72af801e916817560dbe1698028cd0afe5a52d75ceb1fce5f", - "sha256:b911dfb727e247340d36ae20c4b9259e4a64013ab9888ccb3cbba69b77fd9636", - "sha256:b9a794cef1d9c1772b94a72eec6da144c18e18041d294a9ab47669bc77a80c1d", - "sha256:b9c33d4aef08dfecbd1736ceab8b7b3c4358bf10a0121483e5cd60d3d308cc64", - "sha256:b9d38a4656e4e715d637abdf7296e98d6267df0cc0a8e9a016f8ba07e4aa3eeb", - "sha256:bcda1c84a1c533c528356da5490d464a139b6e84eb77cc0b432e38c5c6dd7882", - "sha256:bef7e3f9dc6f0c13afdd671008534be5744e0e682fb851584c8c3a025ec09720", - "sha256:c15ba5982c177bc4b23a7940c7e4394197e2d6a424a2d282e7c236b66da6d896", - "sha256:c5254cbd4f4855e11cebf678c1a848a3042d455a22a4ce61349c36aafd4c2267", - "sha256:c5682a45df7d9642eff590abc73157c887a68f016df0a8ad722dcc0f888f56d7", - "sha256:c5e65c6ac0ae4bf5bef1667029f81010b6017795dcb817ba5c7b8a8d61fab76f", - "sha256:d4c7b3a31502184e856df1f7bbb2c3735a05a8ce0ade34c5277e1577738a5c91", - "sha256:d892bfa1d023c3781a3cab8dd5af76b626c483484d782e8bd047c180db590e4c", - "sha256:dbc332beaf8492b5731229a881807cd7b91b50dbbbaf7fe2faf46942eda64a24", - "sha256:dc85b3777068ed30aff8242be2813038a929f2084f69e43ef869daddae50f6ee", - "sha256:e59137cdb970249ae60be2a49774c6dfb015bd0403f05af1fe61862e9626642d", - "sha256:e67b3c26e9b6d37b370c83aa790bbc121775c57bfb096c2e77eacca25fd0233b", - "sha256:e72c91bda9880f097c8aa3601a2c0de6c708763ba8128006151f496ca9065935", - "sha256:f95b8aca2703d6a30249f83f4fe6a9abf2e627aa892a5caaab2267d56be7ab69" + "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff", + "sha256:056470c3dc57904bbf63d6f534988bafc4e970ffd50f6271fc4ee7daad9498a5", + "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f", + "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5", + "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0", + "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c", + "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c", + "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341", + "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f", + "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", + "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", + "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007", + "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", + "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92", + "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb", + "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5", + "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5", + "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8", + "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1", + "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68", + "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73", + "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1", + "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53", + "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d", + "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906", + "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0", + "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", + "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a", + "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b", + "sha256:5c370b1e4975df846b0277b4deba86419ca77dbc25047f535b0bb03d1a544d44", + "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648", + "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7", + "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f", + "sha256:73aa0e31fa4bb82578f3a6c74a73c273367727de397a7a0f07bd83cbea696baa", + "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697", + "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d", + "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b", + "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526", + "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4", + "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287", + "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e", + "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673", + "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0", + "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30", + "sha256:8aecc5e80c63f7459a1a2ab2c64df952051df196294d9f739933a9f6687e86b3", + "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e", + "sha256:8de718c0e1c4b982a54b41779667242bc630b2197948405b7bd8ce16bcecac92", + "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a", + "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c", + "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8", + "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", + "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47", + "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864", + "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc", + "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00", + "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb", + "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539", + "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b", + "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481", + "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5", + "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4", + "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64", + "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392", + "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4", + "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", + "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1", + "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", + "sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863" ], "index": "pypi", - "version": "==2.9.5" + "version": "==2.9.10" }, - "python-dotenv": { + "pyjwt": { "hashes": [ - "sha256:1c93de8f636cde3ce377292818d0e440b6e45a82f215c3744979151fa8151c49", - "sha256:41e12e0318bebc859fcc4d97d4db8d20ad21721a6aa5047dd59f090391cb549a" + "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", + "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb" ], - "index": "pypi", - "version": "==0.21.1" + "markers": "python_version >= '3.9'", + "version": "==2.10.1" }, - "pyyaml": { + "python-dotenv": { "hashes": [ - "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf", - "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", - "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", - "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", - "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", - "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", - "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", - "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", - "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", - "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", - "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", - "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", - "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782", - "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", - "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", - "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", - "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", - "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", - "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1", - "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", - "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", - "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", - "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", - "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", - "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", - "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d", - "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", - "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", - "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7", - "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", - "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", - "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", - "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358", - "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", - "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", - "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", - "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", - "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f", - "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", - "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" + "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", + "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a" ], - "markers": "python_version >= '3.6'", - "version": "==6.0" + "index": "pypi", + "version": "==1.0.1" }, - "setuptools": { + "pyyaml": { "hashes": [ - "sha256:a7687c12b444eaac951ea87a9627c4f904ac757e7abdc5aac32833234af90378", - "sha256:e261cdf010c11a41cb5cb5f1bf3338a7433832029f559a6a7614bd42a967c300" + "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", + "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", + "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", + "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", + "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", + "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", + "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", + "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", + "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", + "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", + "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", + "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", + "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", + "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", + "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", + "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", + "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", + "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", + "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", + "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", + "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", + "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", + "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", + "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", + "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", + "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", + "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", + "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", + "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", + "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", + "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", + "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", + "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", + "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", + "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", + "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", + "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", + "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", + "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", + "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", + "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", + "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", + "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", + "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", + "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", + "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", + "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", + "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", + "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", + "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", + "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", + "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", + "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" ], - "markers": "python_version >= '3.7'", - "version": "==67.1.0" + "markers": "python_version >= '3.8'", + "version": "==6.0.2" }, "six": { "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", + "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" + "version": "==1.17.0" }, "sqlalchemy": { "hashes": [ @@ -444,35 +504,35 @@ }, "typing-extensions": { "hashes": [ - "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", - "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], "index": "pypi", - "version": "==4.4.0" + "version": "==4.12.2" }, "urllib3": { "hashes": [ - "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72", - "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1" + "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", + "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.26.14" + "markers": "python_version >= '3.9'", + "version": "==2.3.0" }, "werkzeug": { "hashes": [ - "sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f", - "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5" + "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", + "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746" ], - "markers": "python_version >= '3.7'", - "version": "==2.2.2" + "markers": "python_version >= '3.9'", + "version": "==3.1.3" }, "wtforms": { "hashes": [ - "sha256:6b351bbb12dd58af57ffef05bc78425d08d1914e0fd68ee14143b7ade023c5bc", - "sha256:837f2f0e0ca79481b92884962b914eba4e72b7a2daaf1f939c890ed0124b834b" + "sha256:bf831c042829c8cdbad74c27575098d541d039b1faa74c771545ecac916f2c07", + "sha256:f8d76180d7239c94c6322f7990ae1216dae3659b7aa1cee94b6318bdffb474b9" ], - "markers": "python_version >= '3.7'", - "version": "==3.0.1" + "index": "pypi", + "version": "==3.1.2" } }, "develop": {} diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000000..0e04844159 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 0000000000..ec9d45c26a --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000000..4c9709271b --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000000..2c0156303a --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/22b5bf5541c4_.py b/migrations/versions/22b5bf5541c4_.py new file mode 100644 index 0000000000..0e8edd6c96 --- /dev/null +++ b/migrations/versions/22b5bf5541c4_.py @@ -0,0 +1,35 @@ +"""empty message + +Revision ID: 22b5bf5541c4 +Revises: +Create Date: 2025-02-19 19:55:11.728525 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '22b5bf5541c4' +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(length=80), 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/src/front/img/microphone.jpg b/src/front/img/microphone.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1db21ae33c2266bdb2d6fdba74706704a21b7614 GIT binary patch literal 112253 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�sHdJPV45rcQOq%DZJSa)Sv|@*+G5VqFi$n3@eN} zS~@|QSHtI7yQ^;6uGd~-xY3fRNmAk4`uw{SM>vpB6zm4^QZ{DH^?0O4!4@@-u3V@8 zM4R=IzOLZPTQI>xXInhjQ}NbNNe{iKjq~Qa63YrbyT#aJ#6@~Tw!^GM@bxi=50>>N z9=@I*yqI^qKboFBt6St$Q;`sg5g=UDap@eWGhR=KWKt}&<JaKxL(ao2Y`bNjwk_P7 zx5^nm+tgOnTxOhd%nzKMmmb)rocHCDm36IPuNANI(t1XebZyQ!X`B**Sfy2dSjQc1 z{60eqZPGr*t3zvZqk+Zgw;g?dqM%goLkVRYWRDE-+|9Om3o~XlyfWW9#r$9X;r)@Y z3zkaE&vIo(Fzx*s#s^f}uBanaelswvE<6p3vt0TO`%(R-Cp-MP$;Ne#YY54QHmfkH z<x{uA16|Ew-Wz;#@4vfNmW&lVXSmxqX<H{Byd|ReHuw*_R!MP)v#EJ&PQcn9%ZD)@ z4W%!x8jmG&<2jgAYk!i|hd6M2*8AusBIT%W_h)AMdt4x~2gora%HSKbnt=B46@$q~ zndgH)+V9Rsw>N*i*9-mOQ`=yGQ1@Bq80PrMZT;zGoqGPO(S!b9&*wVaQ#yD+Wk-DL zX?i_V^RkXf^RbqOhl9aKkY_LVLsrf09XNkJ7d~`&0BLV6?rnBEtbcaPk9G9NbNNHZ zMuzP}$7}Z~zgtw=vgD0^u6^ncem#EEaq3S{-wN$uLuhr9YKvF8zBA`J?Fqg5!G4;5 zZtl(03(l9?tvW#!rX8mZ7vH1Q&lDvr3G#=W#mw_O?!tYFXlLGdDrQ6NU(Zd<`hQl? zDd(%D;{4O1R<Ey*k+fw*8HcASMbjIHX0QE<6#VAq)8LVJyM8?J@sfQ@w8G#%8&h-J z{!X}KoahWmZxg<v;#s534(s_f*js!A;jkp-ev~{-@bZtIjFTeS_`Q%b8v{B>o}-cd zFB67)m$Mx7_>I@qK}&kzV;=goiO^JRYim7_K3ry@RCV+M(Y4oA(AeNm8}-p8v&GYX zX$QC3Q@B@mk(>LgpjFss>8ElqwA%h)Yn>;o{qmK-(i*cv_@~~wD|gk#tVNz__l<|r zgCA|6>3UPy6a8n`za}Gt!nVmSS{lECyOm#xYM;d2r=mPT>LPld$O{*lsB<yjHS3)g zsyt6K{DV>p>1(fqwggcAuE0T#$;S}|(Oh3*Q#^Irk_YC;w;(@7ItYpA7gWiuS?byw zG8u2Ao(2yIeNT=D#WB=bQWpbETeA7xVIxY!^d@d9`UX;^RvhO-h2Co9^&2&w0dG^9 zVY5E34ofEjzC}EM%*r{QH%!WO-7<E6>=_kE`(T-6!(Lg|;LXzuor0DVZqbU9BFh08 z_w0BAQDmpelp;(^{+!H(E`OE!JH`#An!d5fm;OE*x~k^njh_vKSk+Ji^ff}Bf%DVX zN{fW>y*f9FSCNbV0Hm#l^Wfc@X|unUNl};S0RQ|03&T$6mR))&yL-BKbVD*c6=580 zRg4SDBIs@aHjK(|w$N=Mc`^AsE)Pj$^a1?_1?MFx9gG11)B1TA&TMxmRj(Y;LAHV> z!J+z?;qi%?t{Vguycfw+qtO!A2RA>65RC^5<V-j|MQ&3JlwZs3CQ5gw4=9AvqRPmF z11dY>v8;?>66{C>U@S!@6D}x6Q5B{M<0W#daH0+=0XhnYlTvoU;d#cn02ya}8LUVj z2%Gv81mu(S_m+7YrR8%Gho*Q)`-NI*);R%YMG}}P1j2xFW$uh<fJFqM06{*oS|ca2 zQ4kjM7unB{iX(=m{F@c<-v!~ngM?BlPxOBe4&u2XEJ&m}D9lVnE13XO#FLH9Tojp0 z^e|G^8cAw64Gpw)aQZeIg8v%xC!Rm5CnLIOl>P&LND6{%y#Y0xFeA~WSh$&>l&hpn zUzxL-$0$zFj}wTO5xf_WXGN*YQuI3K95w63=ES?fk|ics;VTZgQ&a-*k@dI0CXWtc zX8B)-Xs)dr7@cm51HGqcErexSDtAE-sY5zmFgB@X`P}}V<zhZ5Q{HJ<;(>mDjeAa_ z7QGE%&MN#iF0w7rVZpRYeWu37IaejAmB<zqJzI^w^}VR_H#@gkU5&|CCrTp?BPC8r zl;LU!vgZ1Gp-YyK+P8xWW-F+#PIb2fCc-P&RV_~Yx3b?odahMydsIn9c<ye{okcY? z7EgcYR>lOUc*#$}vf3|U=n*5GHJRX`P|^6|mBvVyjyGBha6+`u@*de_m(A$2o3h(4 zcLMaA1Zs<JaOMed10CC6)$1Jmkeupm@V#WYZ=~v@kUV~FFJAL@oKbAEcF$98I5<Lb z`Ii0x#wh2ox0&5XHfGVm#Ixp?CXcL9SGBYUsJllU%mA`KPgq{PeO;DwV6i&DxNh9~ zmDW*qNS<tfLN`}>U1(v1c>h_pcK^+@Y&8S@ocClgis)+?RUv_p;_)k2seK5Pp&M@) z6K$l%Ci^Iozf`TDK7_+&Uc}sQ^p^{<emVp=Mdsc`nvymyAdt1H&UBw8^-Z*+0Z%K? zE*W_6)A5pF>AnKP5B%%ZYPs+<&54<MczcQ3{z>{t^!sImF>o!8j4M-ZCu?u%$T1Ng zVB{=!Nfd}aDKVLrcaZ(2R^C0|vW$v6X^lARHeVBU|7be9yAnwm7ZYdMzc7ue6CdVG z=(kM>d=f9)WIEXz>6l39`T8q=(Pm;xb={Bh@YBxy?doM=`lmCKtgma+@Xtt(9z4A; zmVMii%YMVh=BVoJh`7hQ{E<Jb$@lhPhWgj<aU{tlcC#IZ=r)|`SJS+7`O^6Nc1Q@T z?(?!zn0WNXMbTNfSHgQb?~XlWy#fDv6kX#YRkktH9<@KY%iQce^V9OC$;r{8^U8ND zY3|5pqXa#H*eB+P0S`Z$m6zABCg-2=q+406pMDaIW7V<0W;VdJBm_-$2)bYv#N$2D zV#qDUFK4$8cUH{s7LR2L-t`@mrnFS?uvzy!an41gTPkmUZpnC3N*aXx8eGZv#5`B= zs8qfoxGv&aTB?oa(1MGDZ?3{T*G%?$%8ZB-%*Lew=%Ik#^O5xy3bS6*Yl=N!O1DU; zbQ;Zjzfx1g#@)@3ckCm=`Vey^W&*xdqSSDRQtLMXKn8ape{O8^Xf=PvzDnJ9TxiZ| zfOqUNI)tbn%bUHy5l7_+F(ZGFrU~HczWMkrE6Es#7=@|ZrA*FVL{}9<rhVQD^^8<n zlj^J#irsKmGfsab)?08w2og$7?uHJuSD{IbYD2~bKfH%;>5r@F0Lt>YaL<#)fxkF$ z77LP$YC!AYSZ|^eA%B|2Pf9ZF1bfiJ=M(a^t2Q=>7Iws__{tUPLS3+|0+Y_>8s6S1 z#z`Uw4i*M^!FqDeA{FiPhFnjeTd<x{4nt~wS7Y(UsIoys+E6%<Rm+WD(6FE2#<598 zEUdKC>>#+Lkk*;-3)DG*J}ab_lYq3D9xIUu(j?QEaoGU0%SBX86G?<nhS*GJ48N8h zH7^Z{7&Vwx3BW?_D9ofttzU_zz8R$^QpA75N;E}X@FbPI<ghprt7k8$s4Edl4<q#f z$ynZeEnNaxr=T_>GhdJhr1P5rJe0s-A`ta|r|kcNGQ=<cMwk#$6u4BL2(X;$FW^oz zj(Y;J{eL-D+!P>13q+1!n%s@cmf~>A=z<%Q8b`38n=BR&kmO}dl_At+G&q=czRA)v zpguM|`+z`h5=<&;HXyq}eh4Gp8Wc<lz5_>W%%W|)o3$VT5v1<vwykr`+ot1YWLshd zS;HLZcea>HG*gn%i}C|w3h$SUL{9HiYAj8y%T#osSKtYfP#f_=^D{iJ3hcTYAh4;1 z_i36YkE^*~R)}Zy$0?(58n;;ri%GJUyn)wU`;`&9gCYlObs?I`PK!D0wD^L=xu$VR z1SSbOW20$Q?GQQ>@QwKv=lneit*_HctsdWth6YYn4Dw60YZl}jdHa+sb{EoUE{(k3 z&J<t?KX1-W%YB5i>~($#*;8ybHP&<p@iea5n32lA?=|y#G+Nqob|=D)bJv44PD97+ z=%XgBFL!7cJ>9dAXF+0$2mZyYr^~tlx3^4~aMO9U3yfbWpT5nj`jpbAlY%za{m4jR zoFaXfdzONxy*ZmeWi=A;Gt^?USRqk$XlT8#t(Gj`9vw2z&cmErdb7v`{rUwSAEfN( zrf=M`+dWirRGN}}Eg;n|VP|Gh`q=+g?kEk`*W$GGMs*T%vtK<z#U#jW(Uy3nIJ$q= zlQiI1&)jYnaPhpjVvuP5dOS8;K4^7(%(&7}pfI=hZSvtFx;%go?p~pyQ{|l(pcJb_ z7|QnOw!TV9`lFU-F*hME{>^K`<M9tYIri<}&o+~S)!<|$DUs<tx&(a;`Q|68j38pC zAMa-f#W~fs0!38$1nr#dZqtU{^dhL!^V=_9`Yj6I_H9*-v~)*ayALi)!S6iyY3A-N zI!17nn8*Aj03Pq-igjd<Nv|91P=jv2un3Vmt&5jndxxx@CtyoFy4lW7;XOMB*Iy60 zJ)^_0PW!)Ux9^t+R^-od`D`7>5Q*CQf#nPFCpxR^p)U>U=RE2@7DqZyuU7p7xMOy3 z*Cs85{VQvB>G|O`(X^<jRz%mSOHt5Q+vs<Er!d9-d&0CiRk4jc^kda_j`(@C_|9HR zmQn-p4>ev5D$n+gE0h$Cd><*#?R0h+IakL|)>hOFI9IVqdKLKh^tu(Lo?CaN38%(O z6DI4cv}|X?VUuGQ?3+PhhAK7o*}>P;VR4;x9(!gA<j7hNo7_Z17G61qAf#udC(V+b zpYAL3*z1S+uNU8jPqN%%r%eGjr=kv;?)ItF%R|BVN9lo)T-42jur8^u!;QX@)EfoO z{{Rk)e>Q%iC%JU?%lOTl#CFe*dA?2k#AL0tUQ<=F@E<__<uwnpH@WN&Ca;o;$4LAV zpqd@OB`;M)&3VDlye|33n}i7aW1xT2)%Z$e#>1_o_*g?_eKJwrl%6==kAjx|catSU zk=OaROj(l1^Dhy;QlwFjsZC)W%tC0039l7DiSbQ@GZoyB8Z4C|grb1x$dLiW<J|T% zH4t)6M;W6KWBp`k1aDp^br7G=aK)139q=|h4^EDteOQV{pqJ@zTp3}g%8XThSEVtM zr<Ebv=cDmyJ*c3v0pqffz)*OHp46+%$3astVQ*4`yBSm2l2FX39sh!MIzKwo*i0kF zzi1hpo^gQsMznFk^KJvPsDof#V8F#oGO!A?a!bAqQ_GD$!U{FKr%TZ%OIJWS6pI5u z8{TO4!n!Z2OhhCe3MOF$3K(Hx^NkpT!5C=yztxe{Yd~r^9M@w=5r-aSoa+lDTCMro z`n3B{wW~}u#O?Qg!H@qlP5;lA(k^@e6mf+mj_bcxX;PZ0|Mz*h0Q5Jn10V&rn1N?v z1&Iu%78tM`s!4oCT`if;nM%vDj}EM$*#%1OE|X}dCI+&bs3Xbfs<5KRM3Z9jfG@F? z%FOk2jiwSFEEe8$07|%%IUz)$k+UM1BIzU?%qO$d-9n)=2vTlrke20s2&yFfaS@Nu z_9RnNwSa56Zv5WnTi{TVCTxWw6C{@HrSJ3_wVZu2nxadXuSqh4#;PI{<SVz7!Ki3( ze>=+~IrCyf4axf0D=Tt@-6SUh)ZlSAihcT1sPaRFNG}6yfMymxb$A=NhZWD$3EPb_ zb$B?wQJTMdOyi8x^cZn)mJ-KGNa%~qe;96!!FkDH9n72s`w*aJvj`XmPG5_d;HT&l zQ7FM_I>Aq^#q=V}Fq(5@wR*vV9+2<QSZHbbiJJ5QnlD)SzQ>4jbLC9^eBf>%F=nlz zDXHjFFeH_a)_r&n^4?8@A~45q(S?t+lwUIGu7`#+zy^t}uw8MnIbR|lWRK-1#!MnN z;{ZbY_i4xEgNtOiD9f9)AX%@Cz^L$on7N?iVtM5v&CS$qxl+ZH-#5U$IeGmzwV0#! zkhsd_V>gOctx~(KwC|G+?z8IBvaZcyikp12Br*Xx5T%DfnK=-7uMr62ra6diIFA5o z9BONq)Y<ZNHq+N1aQ2^{ZLNHrmY!g<jf1x97!)84U|dHT9;;k~PDfbD7%%*RyTJ0d zCiQK*mmqcdCp>)J?&c&zYnO4LcDH{3e{|Hp{sS-zeQ=dVjP`H-;SRA2qyx2_zpMS0 zReY|+g0{6|3padh3lXyk{y_E}%cp!Rt(1a!7vqii;!nDV6%iB6Xj;oFp3BI@iXQl< zV})ACx<*Jvv>N+>t5U>Q4v?7#jl;q!`bu~Y2GU)rjMC$g_JMp<Mu0P&DJ&Jg6Sx2r zYbvS4jWaB#qM(76Q^%EOIaNP^@{yzwZ*(MCI6i?jGAtidgQ=*T+KTwnXRHB<e>|Vc uKoWrCqmd$)L1F=4s1g4W{6zMi@Bc3)L=_me?>`SEaclW&rNpTJv-CgR!KaJ> literal 0 HcmV?d00001 diff --git a/src/front/js/layout.js b/src/front/js/layout.js index d42289f0ee..fdece58577 100755 --- a/src/front/js/layout.js +++ b/src/front/js/layout.js @@ -1,5 +1,5 @@ import React from "react"; -import { BrowserRouter, Route, Routes } from "react-router-dom"; +import { BrowserRouter, Route, Routes, useLocation } from "react-router-dom"; import ScrollToTop from "./component/scrollToTop"; import { BackendURL } from "./component/backendURL"; @@ -8,6 +8,8 @@ import { Demo } from "./pages/demo"; import { Single } from "./pages/single"; import injectContext from "./store/appContext"; +import { Login } from "./pages/login"; + import { Navbar } from "./component/navbar"; import { Footer } from "./component/footer"; @@ -15,26 +17,41 @@ import { Footer } from "./component/footer"; const Layout = () => { //the basename is used when your project is published in a subdirectory and not in the root of the domain // you can set the basename on the .env file located at the root of this project, E.g: BASENAME=/react-hello-webapp/ - const basename = process.env.BASENAME || ""; - - if(!process.env.BACKEND_URL || process.env.BACKEND_URL == "") return <BackendURL/ >; - - return ( - <div> + const basename = process.env.BASENAME || ""; + + if (!process.env.BACKEND_URL || process.env.BACKEND_URL === "") return <BackendURL />; + + return ( <BrowserRouter basename={basename}> <ScrollToTop> - <Navbar /> - <Routes> - <Route element={<Home />} path="/" /> - <Route element={<Demo />} path="/demo" /> - <Route element={<Single />} path="/single/:theid" /> - <Route element={<h1>Not found!</h1>} /> - </Routes> - <Footer /> + <LayoutContent /> {/* Renderiza el contenido con la lógica de ocultar */} </ScrollToTop> </BrowserRouter> - </div> - ); -}; + ); + }; + + const LayoutContent = () => { + const location = useLocation(); // Obtiene la ruta actual + + const hideNavbarFooter = location.pathname === "/login"; // Solo oculta en /login + + return ( + <> + {!hideNavbarFooter && <Navbar />} + + <Routes> + <Route element={<Home />} path="/" /> + <Route element={<Login />} path="/login" /> + <Route element={<Demo />} path="/demo" /> + <Route element={<Single />} path="/single/:theid" /> + <Route element={<h1>Not found!</h1>} /> + </Routes> + + {!hideNavbarFooter && <Footer />} + </> + ); + }; export default injectContext(Layout); + + diff --git a/src/front/js/pages/login.js b/src/front/js/pages/login.js new file mode 100644 index 0000000000..42242866f1 --- /dev/null +++ b/src/front/js/pages/login.js @@ -0,0 +1,76 @@ +import React, { useState } from "react"; +import "../../styles/login.css"; +import microphone from "../../img/microphone.jpg"; + +export const Login = () => { + // Estado para determinar si mostramos el formulario de login o registro + const [showLogin, setShowLogin] = useState(false); + + // Función para mostrar el formulario de login + const showLoginForm = () => setShowLogin(true); + + // Función para mostrar el formulario de registro + const showRegisterForm = () => setShowLogin(false); + + return ( + <div + style={{ + backgroundImage: `url(${microphone})`, + backgroundSize: "cover", + backgroundPosition: "center", + height: "100vh", + display: "flex", + justifyContent: "center", + alignItems: "center" + }} + > + <div + className="login-container p-4 shadow-lg rounded-3 w-100" + style={{ maxWidth: "400px", background: "rgba(255, 255, 255, 0.5)" }} + > + {!showLogin ? ( + // Pantalla inicial con los botones + <div> + <h2 className="text-center mb-4">Bienvenido</h2> + <button + className="btn btn-primary w-100 mb-3" + onClick={showLoginForm} + > + Iniciar sesión + </button> + <button + className="btn btn-secondary w-100" + onClick={showRegisterForm} + > + Registrarse + </button> + </div> + ) : ( + // Formulario de login + <form> + <h2 className="text-center mb-4">Iniciar sesión</h2> + <div className="mb-3"> + <input + type="text" + className="form-control form-control-lg" + placeholder="Nombre de usuario" + required + /> + </div> + <div className="mb-3"> + <input + type="password" + className="form-control form-control-lg" + placeholder="Contraseña" + required + /> + </div> + <button type="submit" className="btn btn-danger w-100 py-2"> + Ingresar + </button> + </form> + )} + </div> + </div> + ); +}; diff --git a/src/front/styles/home.css b/src/front/styles/home.css index 194b236ac4..7e32217b3e 100755 --- a/src/front/styles/home.css +++ b/src/front/styles/home.css @@ -4,4 +4,12 @@ All pages share the styles on index.css but you should create one more css for each page that will contain the selected used on that page only (the ones not reused in other pages). -*/ \ No newline at end of file +*/ + +body { + background: url(../img/microphone.jpg); + background-size: cover; + background-repeat: no-repeat; + margin: 0; + height: 100vh; + } \ No newline at end of file diff --git a/src/front/styles/login.css b/src/front/styles/login.css new file mode 100644 index 0000000000..342737402e --- /dev/null +++ b/src/front/styles/login.css @@ -0,0 +1,7 @@ +.red { + background: url(../img/microphone.jpg); + background-size: cover; + background-repeat: no-repeat; + margin: 0; + height: 100vh; + } \ No newline at end of file From 16577024197ed2d25cdbd85fd54f5857f757c1e8 Mon Sep 17 00:00:00 2001 From: Jaime BP <jaimebernet@gmail.com> Date: Fri, 21 Feb 2025 18:19:39 +0000 Subject: [PATCH 02/25] First Commit --- Pipfile.lock | 654 +++++++++++++++------------ migrations/README | 1 + migrations/alembic.ini | 50 ++ migrations/env.py | 113 +++++ migrations/script.py.mako | 24 + migrations/versions/34a3df02bba3_.py | 35 ++ src/front/js/component/navbar.js | 41 +- src/front/js/layout.js | 7 + src/front/js/pages/SavedArtists.js | 35 ++ src/front/js/pages/SavedSongs.js | 34 ++ src/front/js/pages/UserProfile.js | 15 + src/front/styles/index.css | 14 +- src/front/styles/navbar.css | 62 +++ src/front/styles/userProfile.css | 68 +++ 14 files changed, 838 insertions(+), 315 deletions(-) create mode 100644 migrations/README create mode 100644 migrations/alembic.ini create mode 100644 migrations/env.py create mode 100644 migrations/script.py.mako create mode 100644 migrations/versions/34a3df02bba3_.py create mode 100644 src/front/js/pages/SavedArtists.js create mode 100644 src/front/js/pages/SavedSongs.js create mode 100644 src/front/js/pages/UserProfile.js create mode 100644 src/front/styles/navbar.css create mode 100644 src/front/styles/userProfile.css diff --git a/Pipfile.lock b/Pipfile.lock index a391864e9d..508225b0a1 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "74f92d76f687bb774828613a3a513123fe2ffdb429b95b351d29721dddfd3fb8" + "sha256": "4f0e9a772f04b621ff0313b7ecfa468af1526aa27df8bfcacac6955d499d352d" }, "pipfile-spec": 6, "requires": { @@ -18,73 +18,91 @@ "default": { "alembic": { "hashes": [ - "sha256:6880dec4f28dd7bd999d2ed13fbe7c9d4337700a44d11a524c0ce0c59aaf0dbd", - "sha256:e8a6ff9f3b1887e1fed68bfb8fb9a000d8f61c21bdcc85b67bb9f87fcbc4fce3" + "sha256:1acdd7a3a478e208b0503cd73614d5e4c6efafa4e73518bb60e4f2846a37b1c5", + "sha256:496e888245a53adf1498fcab31713a469c65836f8de76e01399aa1c3e90dd213" ], - "markers": "python_version >= '3.7'", - "version": "==1.9.2" + "markers": "python_version >= '3.8'", + "version": "==1.14.1" + }, + "blinker": { + "hashes": [ + "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", + "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc" + ], + "markers": "python_version >= '3.9'", + "version": "==1.9.0" }, "certifi": { "hashes": [ - "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", - "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" + "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", + "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe" ], "markers": "python_version >= '3.6'", - "version": "==2022.12.7" + "version": "==2025.1.31" }, "click": { "hashes": [ - "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", - "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" + "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", + "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a" ], "markers": "python_version >= '3.7'", - "version": "==8.1.3" + "version": "==8.1.8" }, "cloudinary": { "hashes": [ - "sha256:f52a1f5eb2c6820f13aa01c109caa5937ad3fd6caf5967817d0ef6c113403afc" + "sha256:ba223705409b2aaddd5196c2184d65f50a83dffcba3b94f3727658ff6a0172a3", + "sha256:e4191b470c5bae55542b64e0a78659af42971880294456dca480bc974fa9280a" ], "index": "pypi", - "version": "==1.31.0" + "version": "==1.42.2" }, "flask": { "hashes": [ - "sha256:642c450d19c4ad482f96729bd2a8f6d32554aa1e231f4f6b4e7e5264b16cca2b", - "sha256:b9c46cc36662a7949f34b52d8ec7bb59c0d74ba08ba6cb9ce9adc1d8676d9526" + "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac", + "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136" ], "index": "pypi", - "version": "==2.2.2" + "version": "==3.1.0" }, "flask-admin": { "hashes": [ - "sha256:424ffc79b7b0dfff051555686ea12e86e48dffacac14beaa319fb4502ac40988" + "sha256:24cae2af832b6a611a01d7dc35f42d266c1d6c75a426b869d8cb241b78233369", + "sha256:fd8190f1ec3355913a22739c46ed3623f1d82b8112cde324c60a6fc9b21c9406" ], "index": "pypi", - "version": "==1.6.0" + "version": "==1.6.1" }, "flask-cors": { "hashes": [ - "sha256:74efc975af1194fc7891ff5cd85b0f7478be4f7f59fe158102e91abb72bb4438", - "sha256:b60839393f3b84a0f3746f6cdca56c1ad7426aa738b70d6c61375857823181de" + "sha256:5aadb4b950c4e93745034594d9f3ea6591f734bb3662e16e255ffbf5e89c88ef", + "sha256:b9e307d082a9261c100d8fb0ba909eec6a228ed1b60a8315fd85f783d61910bc" + ], + "index": "pypi", + "version": "==5.0.0" + }, + "flask-jwt-extended": { + "hashes": [ + "sha256:63a28fc9731bcc6c4b8815b6f954b5904caa534fc2ae9b93b1d3ef12930dca95", + "sha256:9215d05a9413d3855764bcd67035e75819d23af2fafb6b55197eb5a3313fdfb2" ], "index": "pypi", - "version": "==3.0.10" + "version": "==4.6.0" }, "flask-migrate": { "hashes": [ - "sha256:8662a9dd391ce36deeaf3265987319c20fdb4c8a45306a32ba4f8224459abed4", - "sha256:a0062c8d3f32de02847086b46cfc389412f78c71c89a619ebd7097e89d72ea4b" + "sha256:1a336b06eb2c3ace005f5f2ded8641d534c18798d64061f6ff11f79e1434126d", + "sha256:24d8051af161782e0743af1b04a152d007bad9772b2bca67b7ec1e8ceeb3910d" ], "index": "pypi", - "version": "==4.0.3" + "version": "==4.1.0" }, "flask-sqlalchemy": { "hashes": [ - "sha256:2764335f3c9d7ebdc9ed6044afaf98aae9fa50d7a074cef55dde307ec95903ec", - "sha256:add5750b2f9cd10512995261ee2aa23fab85bd5626061aa3c564b33bb4aa780a" + "sha256:c5765e58ca145401b52106c0f46178569243c5da25556be2c231ecc60867c5b1", + "sha256:cabb6600ddd819a9f859f36515bb1bd8e7dbf30206cc679d2b081dff9e383283" ], "index": "pypi", - "version": "==3.0.3" + "version": "==3.0.5" }, "flask-swagger": { "hashes": [ @@ -96,304 +114,346 @@ }, "greenlet": { "hashes": [ - "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a", - "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a", - "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43", - "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33", - "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8", - "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088", - "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca", - "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343", - "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645", - "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db", - "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df", - "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3", - "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86", - "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2", - "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a", - "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf", - "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7", - "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394", - "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40", - "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3", - "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6", - "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74", - "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0", - "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3", - "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91", - "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5", - "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9", - "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8", - "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b", - "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6", - "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb", - "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73", - "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b", - "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df", - "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9", - "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f", - "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0", - "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857", - "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a", - "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249", - "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30", - "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292", - "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b", - "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d", - "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b", - "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c", - "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca", - "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7", - "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75", - "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae", - "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b", - "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470", - "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564", - "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9", - "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099", - "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0", - "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5", - "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19", - "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1", - "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526" + "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' 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": "==2.0.2" + "version": "==3.1.1" }, "gunicorn": { "hashes": [ - "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e", - "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8" + "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", + "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec" ], "index": "pypi", - "version": "==20.1.0" + "version": "==23.0.0" }, "itsdangerous": { "hashes": [ - "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44", - "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a" + "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", + "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173" ], - "markers": "python_version >= '3.7'", - "version": "==2.1.2" + "markers": "python_version >= '3.8'", + "version": "==2.2.0" }, "jinja2": { "hashes": [ - "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", - "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" + "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", + "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb" ], "markers": "python_version >= '3.7'", - "version": "==3.1.2" + "version": "==3.1.5" }, "mako": { "hashes": [ - "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818", - "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34" + "sha256:95920acccb578427a9aa38e37a186b1e43156c87260d7ba18ca63aa4c7cbd3a1", + "sha256:b5d65ff3462870feec922dbccf38f6efb44e5714d7b593a656be86663d8600ac" ], - "markers": "python_version >= '3.7'", - "version": "==1.2.4" + "markers": "python_version >= '3.8'", + "version": "==1.3.9" }, "markupsafe": { "hashes": [ - "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed", - "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc", - "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2", - "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460", - "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7", - "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0", - "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1", - "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa", - "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03", - "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323", - "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65", - "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013", - "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036", - "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f", - "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4", - "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419", - "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2", - "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619", - "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a", - "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a", - "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd", - "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7", - "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666", - "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65", - "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859", - "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625", - "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff", - "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156", - "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd", - "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba", - "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f", - "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1", - "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094", - "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a", - "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513", - "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed", - "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d", - "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3", - "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147", - "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c", - "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603", - "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601", - "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a", - "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1", - "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d", - "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3", - "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54", - "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2", - "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6", - "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58" + "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", + "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", + "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", + "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", + "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", + "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", + "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", + "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", + "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", + "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", + "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", + "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", + "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", + "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", + "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", + "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", + "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", + "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", + "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", + "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", + "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", + "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", + "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", + "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", + "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", + "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", + "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", + "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", + "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", + "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", + "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", + "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", + "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", + "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", + "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", + "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", + "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", + "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", + "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", + "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", + "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", + "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", + "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", + "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", + "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", + "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", + "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", + "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", + "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", + "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", + "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", + "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", + "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", + "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", + "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", + "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", + "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", + "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", + "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", + "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", + "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50" ], - "markers": "python_version >= '3.7'", - "version": "==2.1.2" + "markers": "python_version >= '3.9'", + "version": "==3.0.2" + }, + "packaging": { + "hashes": [ + "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", + "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" + ], + "markers": "python_version >= '3.8'", + "version": "==24.2" }, "psycopg2-binary": { "hashes": [ - "sha256:00475004e5ed3e3bf5e056d66e5dcdf41a0dc62efcd57997acd9135c40a08a50", - "sha256:01ad49d68dd8c5362e4bfb4158f2896dc6e0c02e87b8a3770fc003459f1a4425", - "sha256:024030b13bdcbd53d8a93891a2cf07719715724fc9fee40243f3bd78b4264b8f", - "sha256:02551647542f2bf89073d129c73c05a25c372fc0a49aa50e0de65c3c143d8bd0", - "sha256:043a9fd45a03858ff72364b4b75090679bd875ee44df9c0613dc862ca6b98460", - "sha256:05b3d479425e047c848b9782cd7aac9c6727ce23181eb9647baf64ffdfc3da41", - "sha256:0775d6252ccb22b15da3b5d7adbbf8cfe284916b14b6dc0ff503a23edb01ee85", - "sha256:1764546ffeaed4f9428707be61d68972eb5ede81239b46a45843e0071104d0dd", - "sha256:1e491e6489a6cb1d079df8eaa15957c277fdedb102b6a68cfbf40c4994412fd0", - "sha256:212757ffcecb3e1a5338d4e6761bf9c04f750e7d027117e74aa3cd8a75bb6fbd", - "sha256:215d6bf7e66732a514f47614f828d8c0aaac9a648c46a831955cb103473c7147", - "sha256:25382c7d174c679ce6927c16b6fbb68b10e56ee44b1acb40671e02d29f2fce7c", - "sha256:2abccab84d057723d2ca8f99ff7b619285d40da6814d50366f61f0fc385c3903", - "sha256:2d964eb24c8b021623df1c93c626671420c6efadbdb8655cb2bd5e0c6fa422ba", - "sha256:2ec46ed947801652c9643e0b1dc334cfb2781232e375ba97312c2fc256597632", - "sha256:2ef892cabdccefe577088a79580301f09f2a713eb239f4f9f62b2b29cafb0577", - "sha256:33e632d0885b95a8b97165899006c40e9ecdc634a529dca7b991eb7de4ece41c", - "sha256:3520d7af1ebc838cc6084a3281145d5cd5bdd43fdef139e6db5af01b92596cb7", - "sha256:3d790f84201c3698d1bfb404c917f36e40531577a6dda02e45ba29b64d539867", - "sha256:3fc33295cfccad697a97a76dec3f1e94ad848b7b163c3228c1636977966b51e2", - "sha256:422e3d43b47ac20141bc84b3d342eead8d8099a62881a501e97d15f6addabfe9", - "sha256:426c2ae999135d64e6a18849a7d1ad0e1bd007277e4a8f4752eaa40a96b550ff", - "sha256:46512486be6fbceef51d7660dec017394ba3e170299d1dc30928cbedebbf103a", - "sha256:46850a640df62ae940e34a163f72e26aca1f88e2da79148e1862faaac985c302", - "sha256:484405b883630f3e74ed32041a87456c5e0e63a8e3429aa93e8714c366d62bd1", - "sha256:4e7904d1920c0c89105c0517dc7e3f5c20fb4e56ba9cdef13048db76947f1d79", - "sha256:56b2957a145f816726b109ee3d4e6822c23f919a7d91af5a94593723ed667835", - "sha256:5c6527c8efa5226a9e787507652dd5ba97b62d29b53c371a85cd13f957fe4d42", - "sha256:5cbc554ba47ecca8cd3396ddaca85e1ecfe3e48dd57dc5e415e59551affe568e", - "sha256:5d28ecdf191db558d0c07d0f16524ee9d67896edf2b7990eea800abeb23ebd61", - "sha256:5fc447058d083b8c6ac076fc26b446d44f0145308465d745fba93a28c14c9e32", - "sha256:63e318dbe52709ed10d516a356f22a635e07a2e34c68145484ed96a19b0c4c68", - "sha256:68d81a2fe184030aa0c5c11e518292e15d342a667184d91e30644c9d533e53e1", - "sha256:6e63814ec71db9bdb42905c925639f319c80e7909fb76c3b84edc79dadef8d60", - "sha256:6f8a9bcab7b6db2e3dbf65b214dfc795b4c6b3bb3af922901b6a67f7cb47d5f8", - "sha256:70831e03bd53702c941da1a1ad36c17d825a24fbb26857b40913d58df82ec18b", - "sha256:74eddec4537ab1f701a1647214734bc52cee2794df748f6ae5908e00771f180a", - "sha256:7b3751857da3e224f5629400736a7b11e940b5da5f95fa631d86219a1beaafec", - "sha256:7cf1d44e710ca3a9ce952bda2855830fe9f9017ed6259e01fcd71ea6287565f5", - "sha256:7d07f552d1e412f4b4e64ce386d4c777a41da3b33f7098b6219012ba534fb2c2", - "sha256:7d88db096fa19d94f433420eaaf9f3c45382da2dd014b93e4bf3215639047c16", - "sha256:7ee3095d02d6f38bd7d9a5358fcc9ea78fcdb7176921528dd709cc63f40184f5", - "sha256:902844f9c4fb19b17dfa84d9e2ca053d4a4ba265723d62ea5c9c26b38e0aa1e6", - "sha256:937880290775033a743f4836aa253087b85e62784b63fd099ee725d567a48aa1", - "sha256:95076399ec3b27a8f7fa1cc9a83417b1c920d55cf7a97f718a94efbb96c7f503", - "sha256:9c38d3869238e9d3409239bc05bc27d6b7c99c2a460ea337d2814b35fb4fea1b", - "sha256:9e32cedc389bcb76d9f24ea8a012b3cb8385ee362ea437e1d012ffaed106c17d", - "sha256:9ffdc51001136b699f9563b1c74cc1f8c07f66ef7219beb6417a4c8aaa896c28", - "sha256:a0adef094c49f242122bb145c3c8af442070dc0e4312db17e49058c1702606d4", - "sha256:a36a0e791805aa136e9cbd0ffa040d09adec8610453ee8a753f23481a0057af5", - "sha256:a7e518a0911c50f60313cb9e74a169a65b5d293770db4770ebf004245f24b5c5", - "sha256:af0516e1711995cb08dc19bbd05bec7dbdebf4185f68870595156718d237df3e", - "sha256:b8104f709590fff72af801e916817560dbe1698028cd0afe5a52d75ceb1fce5f", - "sha256:b911dfb727e247340d36ae20c4b9259e4a64013ab9888ccb3cbba69b77fd9636", - "sha256:b9a794cef1d9c1772b94a72eec6da144c18e18041d294a9ab47669bc77a80c1d", - "sha256:b9c33d4aef08dfecbd1736ceab8b7b3c4358bf10a0121483e5cd60d3d308cc64", - "sha256:b9d38a4656e4e715d637abdf7296e98d6267df0cc0a8e9a016f8ba07e4aa3eeb", - "sha256:bcda1c84a1c533c528356da5490d464a139b6e84eb77cc0b432e38c5c6dd7882", - "sha256:bef7e3f9dc6f0c13afdd671008534be5744e0e682fb851584c8c3a025ec09720", - "sha256:c15ba5982c177bc4b23a7940c7e4394197e2d6a424a2d282e7c236b66da6d896", - "sha256:c5254cbd4f4855e11cebf678c1a848a3042d455a22a4ce61349c36aafd4c2267", - "sha256:c5682a45df7d9642eff590abc73157c887a68f016df0a8ad722dcc0f888f56d7", - "sha256:c5e65c6ac0ae4bf5bef1667029f81010b6017795dcb817ba5c7b8a8d61fab76f", - "sha256:d4c7b3a31502184e856df1f7bbb2c3735a05a8ce0ade34c5277e1577738a5c91", - "sha256:d892bfa1d023c3781a3cab8dd5af76b626c483484d782e8bd047c180db590e4c", - "sha256:dbc332beaf8492b5731229a881807cd7b91b50dbbbaf7fe2faf46942eda64a24", - "sha256:dc85b3777068ed30aff8242be2813038a929f2084f69e43ef869daddae50f6ee", - "sha256:e59137cdb970249ae60be2a49774c6dfb015bd0403f05af1fe61862e9626642d", - "sha256:e67b3c26e9b6d37b370c83aa790bbc121775c57bfb096c2e77eacca25fd0233b", - "sha256:e72c91bda9880f097c8aa3601a2c0de6c708763ba8128006151f496ca9065935", - "sha256:f95b8aca2703d6a30249f83f4fe6a9abf2e627aa892a5caaab2267d56be7ab69" + "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff", + "sha256:056470c3dc57904bbf63d6f534988bafc4e970ffd50f6271fc4ee7daad9498a5", + "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f", + "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5", + "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0", + "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c", + "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c", + "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341", + "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f", + "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", + "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", + "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007", + "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", + "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92", + "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb", + "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5", + "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5", + "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8", + "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1", + "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68", + "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73", + "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1", + "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53", + "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d", + "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906", + "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0", + "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", + "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a", + "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b", + "sha256:5c370b1e4975df846b0277b4deba86419ca77dbc25047f535b0bb03d1a544d44", + "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648", + "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7", + "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f", + "sha256:73aa0e31fa4bb82578f3a6c74a73c273367727de397a7a0f07bd83cbea696baa", + "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697", + "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d", + "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b", + "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526", + "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4", + "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287", + "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e", + "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673", + "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0", + "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30", + "sha256:8aecc5e80c63f7459a1a2ab2c64df952051df196294d9f739933a9f6687e86b3", + "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e", + "sha256:8de718c0e1c4b982a54b41779667242bc630b2197948405b7bd8ce16bcecac92", + "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a", + "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c", + "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8", + "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", + "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47", + "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864", + "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc", + "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00", + "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb", + "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539", + "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b", + "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481", + "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5", + "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4", + "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64", + "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392", + "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4", + "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", + "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1", + "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", + "sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863" ], "index": "pypi", - "version": "==2.9.5" + "version": "==2.9.10" }, - "python-dotenv": { + "pyjwt": { "hashes": [ - "sha256:1c93de8f636cde3ce377292818d0e440b6e45a82f215c3744979151fa8151c49", - "sha256:41e12e0318bebc859fcc4d97d4db8d20ad21721a6aa5047dd59f090391cb549a" + "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", + "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb" ], - "index": "pypi", - "version": "==0.21.1" + "markers": "python_version >= '3.9'", + "version": "==2.10.1" }, - "pyyaml": { + "python-dotenv": { "hashes": [ - "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf", - "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", - "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", - "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", - "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", - "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", - "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", - "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", - "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", - "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", - "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", - "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", - "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782", - "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", - "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", - "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", - "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", - "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", - "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1", - "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", - "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", - "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", - "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", - "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", - "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", - "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d", - "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", - "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", - "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7", - "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", - "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", - "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", - "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358", - "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", - "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", - "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", - "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", - "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f", - "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", - "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" + "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", + "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a" ], - "markers": "python_version >= '3.6'", - "version": "==6.0" + "index": "pypi", + "version": "==1.0.1" }, - "setuptools": { + "pyyaml": { "hashes": [ - "sha256:a7687c12b444eaac951ea87a9627c4f904ac757e7abdc5aac32833234af90378", - "sha256:e261cdf010c11a41cb5cb5f1bf3338a7433832029f559a6a7614bd42a967c300" + "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", + "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", + "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", + "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", + "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", + "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", + "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", + "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", + "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", + "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", + "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", + "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", + "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", + "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", + "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", + "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", + "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", + "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", + "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", + "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", + "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", + "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", + "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", + "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", + "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", + "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", + "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", + "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", + "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", + "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", + "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", + "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", + "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", + "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", + "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", + "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", + "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", + "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", + "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", + "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", + "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", + "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", + "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", + "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", + "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", + "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", + "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", + "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", + "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", + "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", + "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", + "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", + "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" ], - "markers": "python_version >= '3.7'", - "version": "==67.1.0" + "markers": "python_version >= '3.8'", + "version": "==6.0.2" }, "six": { "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", + "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" + "version": "==1.17.0" }, "sqlalchemy": { "hashes": [ @@ -444,35 +504,35 @@ }, "typing-extensions": { "hashes": [ - "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", - "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], "index": "pypi", - "version": "==4.4.0" + "version": "==4.12.2" }, "urllib3": { "hashes": [ - "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72", - "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1" + "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", + "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.26.14" + "markers": "python_version >= '3.9'", + "version": "==2.3.0" }, "werkzeug": { "hashes": [ - "sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f", - "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5" + "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", + "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746" ], - "markers": "python_version >= '3.7'", - "version": "==2.2.2" + "markers": "python_version >= '3.9'", + "version": "==3.1.3" }, "wtforms": { "hashes": [ - "sha256:6b351bbb12dd58af57ffef05bc78425d08d1914e0fd68ee14143b7ade023c5bc", - "sha256:837f2f0e0ca79481b92884962b914eba4e72b7a2daaf1f939c890ed0124b834b" + "sha256:bf831c042829c8cdbad74c27575098d541d039b1faa74c771545ecac916f2c07", + "sha256:f8d76180d7239c94c6322f7990ae1216dae3659b7aa1cee94b6318bdffb474b9" ], - "markers": "python_version >= '3.7'", - "version": "==3.0.1" + "index": "pypi", + "version": "==3.1.2" } }, "develop": {} diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000000..0e04844159 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 0000000000..ec9d45c26a --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000000..4c9709271b --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000000..2c0156303a --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/34a3df02bba3_.py b/migrations/versions/34a3df02bba3_.py new file mode 100644 index 0000000000..feceaccec3 --- /dev/null +++ b/migrations/versions/34a3df02bba3_.py @@ -0,0 +1,35 @@ +"""empty message + +Revision ID: 34a3df02bba3 +Revises: +Create Date: 2025-02-21 09:53:15.839016 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '34a3df02bba3' +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(length=80), 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/src/front/js/component/navbar.js b/src/front/js/component/navbar.js index af4b01e334..3be878d86e 100755 --- a/src/front/js/component/navbar.js +++ b/src/front/js/component/navbar.js @@ -1,19 +1,30 @@ -import React from "react"; +import React, { useState } from "react"; import { Link } from "react-router-dom"; +import "../../styles/navbar.css"; export const Navbar = () => { - return ( - <nav className="navbar navbar-light bg-light"> - <div className="container"> - <Link to="/"> - <span className="navbar-brand mb-0 h1">React Boilerplate</span> - </Link> - <div className="ml-auto"> - <Link to="/demo"> - <button className="btn btn-primary">Check the Context in action</button> - </Link> - </div> - </div> - </nav> - ); + const [menuOpen, setMenuOpen] = useState(false); + + const toggleMenu = () => { + setMenuOpen(!menuOpen); + }; + + return ( + <nav className="navbar"> + <div className="container"> + <Link to="/" className="navbar-brand">SoundCript</Link> + + {/* Botón de menú hamburguesa */} + <div className="menu-icon" onClick={toggleMenu}> + ☰ + </div> + + {/* Menú desplegable */} + <div className={`dropdown-menu ${menuOpen ? "show" : ""}`}> + <Link to="/userProfile" className="dropdown-item" onClick={() => setMenuOpen(false)}>Perfil</Link> + <Link to="/logout" className="dropdown-item" onClick={() => setMenuOpen(false)}>Logout</Link> + </div> + </div> + </nav> + ); }; diff --git a/src/front/js/layout.js b/src/front/js/layout.js index d42289f0ee..1387025646 100755 --- a/src/front/js/layout.js +++ b/src/front/js/layout.js @@ -8,6 +8,10 @@ import { Demo } from "./pages/demo"; import { Single } from "./pages/single"; import injectContext from "./store/appContext"; +import { UserProfile } from "./pages/UserProfile"; // ✅ Nueva vista +import { SavedSongs } from "./pages/SavedSongs"; // ✅ Nueva vista +import { SavedArtists } from "./pages/SavedArtists"; // ✅ Nueva vista + import { Navbar } from "./component/navbar"; import { Footer } from "./component/footer"; @@ -28,6 +32,9 @@ const Layout = () => { <Route element={<Home />} path="/" /> <Route element={<Demo />} path="/demo" /> <Route element={<Single />} path="/single/:theid" /> + <Route element={<UserProfile />} path="/userProfile" /> {/* ✅ nueva */} + <Route element={<SavedSongs />} path="/savedSongs" /> {/* ✅ nueva */} + <Route element={<SavedArtists />} path="/savedArtists" /> {/* ✅ nueva */} <Route element={<h1>Not found!</h1>} /> </Routes> <Footer /> diff --git a/src/front/js/pages/SavedArtists.js b/src/front/js/pages/SavedArtists.js new file mode 100644 index 0000000000..6d76f72787 --- /dev/null +++ b/src/front/js/pages/SavedArtists.js @@ -0,0 +1,35 @@ +import React, { useState } from "react"; +import { Link } from "react-router-dom"; +import "../../styles/userProfile.css"; + +export const SavedArtists = () => { + const [artists, setArtists] = useState([ + { id: 1, name: "Artist A", image: "https://via.placeholder.com/50" }, + { id: 2, name: "Artist B", image: "https://via.placeholder.com/50" } + ]); + + const removeArtist = (id) => { + setArtists(artists.filter(artist => artist.id !== id)); + }; + + return ( + <div className="profile-container"> + <h2>🎤 Artistas Seguidos</h2> + <div className="options"> + <Link to="/savedSongs" className="option-button">🎵 Canciones Guardadas</Link> + <Link to="/savedArtists" className="option-button active">🎤 Artistas Seguidos</Link> + </div> + {artists.length === 0 ? <p>No sigues a ningún artista.</p> : ( + <ul> + {artists.map(artist => ( + <li key={artist.id}> + <img src={artist.image} alt={artist.name} /> + {artist.name} + <button onClick={() => removeArtist(artist.id)}>❌</button> + </li> + ))} + </ul> + )} + </div> + ); +}; diff --git a/src/front/js/pages/SavedSongs.js b/src/front/js/pages/SavedSongs.js new file mode 100644 index 0000000000..d99dd283b0 --- /dev/null +++ b/src/front/js/pages/SavedSongs.js @@ -0,0 +1,34 @@ +import React, { useState } from "react"; +import { Link } from "react-router-dom"; +import "../../styles/userProfile.css"; + +export const SavedSongs = () => { + const [songs, setSongs] = useState([ + { id: 1, title: "Song 1", artist: "Artist A" }, + { id: 2, title: "Song 2", artist: "Artist B" } + ]); + + const removeSong = (id) => { + setSongs(songs.filter(song => song.id !== id)); + }; + + return ( + <div className="profile-container"> + <h2>🎵 Canciones Guardadas</h2> + <div className="options"> + <Link to="/savedSongs" className="option-button active">🎵 Canciones Guardadas</Link> + <Link to="/savedArtists" className="option-button">🎤 Artistas Seguidos</Link> + </div> + {songs.length === 0 ? <p>No tienes canciones guardadas.</p> : ( + <ul> + {songs.map(song => ( + <li key={song.id}> + {song.title} - {song.artist} + <button onClick={() => removeSong(song.id)}>❌</button> + </li> + ))} + </ul> + )} + </div> + ); +}; diff --git a/src/front/js/pages/UserProfile.js b/src/front/js/pages/UserProfile.js new file mode 100644 index 0000000000..7e498de766 --- /dev/null +++ b/src/front/js/pages/UserProfile.js @@ -0,0 +1,15 @@ +import React, { useEffect } from "react"; +import { useNavigate } from "react-router-dom"; + +export const UserProfile = () => { + const navigate = useNavigate(); + + useEffect(() => { + navigate("/savedSongs"); // Redirige automáticamente a canciones guardadas + }, []); + + return null; // No renderiza nada, solo redirige +}; + +export default UserProfile; + diff --git a/src/front/styles/index.css b/src/front/styles/index.css index 1ac0e879a5..f8e9abc3f9 100755 --- a/src/front/styles/index.css +++ b/src/front/styles/index.css @@ -1,3 +1,11 @@ -/* - General Styles used on every website (Don't Repeat Yourself) -*/ +:root { + --color-white: #FFFFFF; + --color-black: #0C121C; + --color-red: #B80000; + --color-coquelicot: #FF4E2A; + --color-giants-orange: #FF6733; + --color-orange-crayola: #FF7E3C; + --color-atomic-tangerine: #FF914D; + --color-green: #00B85F; + --color-mauveine: #A328A3; +} \ No newline at end of file diff --git a/src/front/styles/navbar.css b/src/front/styles/navbar.css new file mode 100644 index 0000000000..873ef3845d --- /dev/null +++ b/src/front/styles/navbar.css @@ -0,0 +1,62 @@ +.navbar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px; + background: var(--color-black); + color: var(--color-white); + position: relative; +} + +.navbar-brand { + text-decoration: none; + color: var(--color-white); + font-size: 1.5rem; + font-weight: bold; +} + +/* Menú hamburguesa */ +.menu-icon { + font-size: 2rem; + cursor: pointer; + user-select: none; +} + +/* Menú desplegable */ +.dropdown-menu { + position: absolute; + top: 50px; + right: 10px; + background: var(--color-white); + color: var(--color-black); + border-radius: 5px; + box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); + width: 150px; + text-align: center; + padding: 10px 0; + opacity: 0; + visibility: hidden; + transform: translateY(-10px); + transition: opacity 0.3s ease, transform 0.3s ease, visibility 0.3s; +} + +/* Mostrar menú */ +.dropdown-menu.show { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +/* Botones dentro del menú */ +.dropdown-item { + display: block; + padding: 10px; + text-decoration: none; + color: var(--color-black); + font-weight: bold; + transition: background 0.2s; +} + +.dropdown-item:hover { + background: var(--color-atomic-tangerine); +} diff --git a/src/front/styles/userProfile.css b/src/front/styles/userProfile.css new file mode 100644 index 0000000000..ebcea8e0f2 --- /dev/null +++ b/src/front/styles/userProfile.css @@ -0,0 +1,68 @@ +.profile-container { + max-width: 600px; + margin: auto; + padding: 20px; + text-align: center; + background-color: var(--color-white); + color: var(--color-black); + border-radius: 10px; + box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); +} + +/* Botones de navegación */ +.options { + display: flex; + justify-content: center; + gap: 15px; + margin-bottom: 20px; +} + +.option-button { + text-decoration: none; + padding: 10px 15px; + background: var(--color-coquelicot); + color: var(--color-white); + border-radius: 5px; + font-weight: bold; + transition: background 0.3s ease; +} + +.option-button:hover { + background: var(--color-giants-orange); +} + +.option-button.active { + background: var(--color-black); + color: var(--color-white); +} + +/* Lista de canciones y artistas */ +ul { + list-style: none; + padding: 0; +} + +li { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px; + border-bottom: 1px solid var(--color-black); + background: var(--color-orange-crayola); + border-radius: 5px; + margin-bottom: 5px; + color: var(--color-black); +} + +button { + border: none; + background: var(--color-red); + color: var(--color-white); + padding: 5px 10px; + cursor: pointer; + border-radius: 5px; +} + +button:hover { + background: var(--color-mauveine); +} From 17c2b388070cf06975500e619bf76fb8a033ab38 Mon Sep 17 00:00:00 2001 From: Oscar Parra <oscarparrapalomino@gmail.com> Date: Mon, 24 Feb 2025 19:09:55 +0000 Subject: [PATCH 03/25] First Commit --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 80704f4378..12e3b32354 100755 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,4 @@ database.database database.db diagram.png __pycache__/ +migrations \ No newline at end of file From 8c469fbe0c33d19bd9d613b28d7212a9dc5b76a7 Mon Sep 17 00:00:00 2001 From: Oscar Parra <oscarparrapalomino@gmail.com> Date: Mon, 24 Feb 2025 20:04:54 +0000 Subject: [PATCH 04/25] Final Commit --- src/front/js/pages/login.js | 111 +++++++++++++++++++++++++++++++----- 1 file changed, 97 insertions(+), 14 deletions(-) diff --git a/src/front/js/pages/login.js b/src/front/js/pages/login.js index 42242866f1..b1279ff4d7 100644 --- a/src/front/js/pages/login.js +++ b/src/front/js/pages/login.js @@ -3,14 +3,9 @@ import "../../styles/login.css"; import microphone from "../../img/microphone.jpg"; export const Login = () => { - // Estado para determinar si mostramos el formulario de login o registro + // Estados para controlar qué formulario se muestra const [showLogin, setShowLogin] = useState(false); - - // Función para mostrar el formulario de login - const showLoginForm = () => setShowLogin(true); - - // Función para mostrar el formulario de registro - const showRegisterForm = () => setShowLogin(false); + const [showRegister, setShowRegister] = useState(false); return ( <div @@ -26,27 +21,36 @@ export const Login = () => { > <div className="login-container p-4 shadow-lg rounded-3 w-100" - style={{ maxWidth: "400px", background: "rgba(255, 255, 255, 0.5)" }} + style={{ maxWidth: "400px", background: "rgba(255, 255, 255, 0.8)" }} > - {!showLogin ? ( - // Pantalla inicial con los botones + {/* Pantalla inicial con los botones */} + {!showLogin && !showRegister && ( <div> <h2 className="text-center mb-4">Bienvenido</h2> <button className="btn btn-primary w-100 mb-3" - onClick={showLoginForm} + onClick={() => { + setShowLogin(true); + setShowRegister(false); + }} > Iniciar sesión </button> + <button className="btn btn-secondary w-100" - onClick={showRegisterForm} + onClick={() => { + setShowLogin(false); + setShowRegister(true); + }} > Registrarse </button> </div> - ) : ( - // Formulario de login + )} + + {/* Formulario de inicio de sesión */} + {showLogin && ( <form> <h2 className="text-center mb-4">Iniciar sesión</h2> <div className="mb-3"> @@ -68,6 +72,85 @@ export const Login = () => { <button type="submit" className="btn btn-danger w-100 py-2"> Ingresar </button> + <div className="text-center mt-2"> + <a href="#" className="text-decoration-none">He olvidado mi contraseña</a> + </div> + <button + type="button" + className="btn btn-link w-100 mt-2" + onClick={() => setShowLogin(false)} + > + Volver + </button> + </form> + )} + + {/* Formulario de registro */} + {showRegister && ( + <form> + <h2 className="text-center mb-4">Registro</h2> + <div className="mb-3"> + <input + type="text" + className="form-control form-control-lg" + placeholder="Nombre y apellidos" + required + /> + </div> + <div className="mb-3"> + <input + type="text" + className="form-control form-control-lg" + placeholder="Nombre de usuario" + required + /> + </div> + <div className="mb-3"> + <input + type="email" + className="form-control form-control-lg" + placeholder="Correo electrónico" + required + /> + </div> + <div className="mb-3"> + <input + type="text" + className="form-control form-control-lg" + placeholder="Dirección" + required + /> + </div> + <div className="mb-3"> + <input + type="password" + className="form-control form-control-lg" + placeholder="Contraseña" + required + /> + </div> + <div className="mb-3"> + <input + type="password" + className="form-control form-control-lg" + placeholder="Confirmar contraseña" + required + /> + </div> + <div className="mb-3"> + <label className="form-label">Subir foto de perfil</label> + <input type="file" className="form-control form-control-lg" /> + </div> + <button type="submit" className="btn btn-success w-100 py-2"> + Registrarse + </button> + <button + type="button" + className="btn btn-link w-100 mt-2" + onClick={() => setShowRegister(false)} + > + Volver + </button> </form> )} </div> From b846874590250648c3840cfcead8ee8080a68df3 Mon Sep 17 00:00:00 2001 From: Oscar Parra <oscarparrapalomino@gmail.com> Date: Tue, 25 Feb 2025 04:03:07 +0000 Subject: [PATCH 05/25] First commit --- src/front/js/pages/login.js | 63 ++++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 11 deletions(-) diff --git a/src/front/js/pages/login.js b/src/front/js/pages/login.js index b1279ff4d7..5ea9811bde 100644 --- a/src/front/js/pages/login.js +++ b/src/front/js/pages/login.js @@ -6,6 +6,7 @@ export const Login = () => { // Estados para controlar qué formulario se muestra const [showLogin, setShowLogin] = useState(false); const [showRegister, setShowRegister] = useState(false); + const [showForgoten, setShowForgoten] = useState(false); return ( <div @@ -36,7 +37,7 @@ export const Login = () => { > Iniciar sesión </button> - + <button className="btn btn-secondary w-100" onClick={() => { @@ -50,7 +51,7 @@ export const Login = () => { )} {/* Formulario de inicio de sesión */} - {showLogin && ( + {showLogin && !showForgoten && ( <form> <h2 className="text-center mb-4">Iniciar sesión</h2> <div className="mb-3"> @@ -73,15 +74,21 @@ export const Login = () => { Ingresar </button> <div className="text-center mt-2"> - <a href="#" className="text-decoration-none">He olvidado mi contraseña</a> + <a href="#" className="text-decoration-none" onClick={() => + setShowForgoten(true) + }>¿He olvidado mi contraseña?</a> + <button + type="button" + className="btn btn-link w-100 mt-2" + onClick={() => { + setShowForgoten(false); + setShowLogin(false); + setShowRegister(false) + }} + > + Volver + </button> </div> - <button - type="button" - className="btn btn-link w-100 mt-2" - onClick={() => setShowLogin(false)} - > - Volver - </button> </form> )} @@ -153,7 +160,41 @@ export const Login = () => { </button> </form> )} + {/* Formulario de recuperar contraseña*/} + {showForgoten && ( + <form> + <h2 className="text-center mb-4">¿He olvidado mi contraseña?</h2> + <div className="mb-3"> + <input + type="text" + className="form-control form-control-lg" + placeholder="Nomber de usuario" + required + /> + </div> + <div className="mb-3"> + <input + type="password" + className="form-control form-control-lg" + placeholder="Nueva contraseña" + required + /> + </div> + <button + type="button" + className="btn btn-link w-100 mt-2" + onClick={() => { + setShowLogin(true); + setShowRegister(false); + setShowForgoten(false) + }} + > + Volver + </button> + </form>)} + + </div> - </div> + </div > ); }; From 01d196d418bbdf06aacd44ef33f48b9686587a3f Mon Sep 17 00:00:00 2001 From: Rafael Salinas Gonzalez <rafsalgon@gmail.com> Date: Wed, 26 Feb 2025 18:32:07 +0000 Subject: [PATCH 06/25] Genre view --- src/front/js/layout.js | 2 ++ src/front/js/pages/genre.js | 69 +++++++++++++++++++++++++++++++++++++ src/front/styles/genre.css | 37 ++++++++++++++++++++ 3 files changed, 108 insertions(+) create mode 100644 src/front/js/pages/genre.js create mode 100644 src/front/styles/genre.css diff --git a/src/front/js/layout.js b/src/front/js/layout.js index d135ebefc2..054b2f785b 100755 --- a/src/front/js/layout.js +++ b/src/front/js/layout.js @@ -12,6 +12,7 @@ import { Login } from "./pages/login"; import { UserProfile } from "./pages/UserProfile"; // ✅ Nueva vista import { SavedSongs } from "./pages/SavedSongs"; // ✅ Nueva vista import { SavedArtists } from "./pages/SavedArtists"; // ✅ Nueva vista +import { Genre } from "./pages/genre"; import { Navbar } from "./component/navbar"; import { Footer } from "./component/footer"; @@ -47,6 +48,7 @@ const LayoutContent = () => { <Route element={<Login />} path="/login" /> <Route element={<Demo />} path="/demo" /> <Route element={<Single />} path="/single/:theid" /> + <Route element={<Genre />} path="/genre" /> <Route element={<UserProfile />} path="/userProfile" /> {/* ✅ nueva */} <Route element={<SavedSongs />} path="/savedSongs" /> {/* ✅ nueva */} <Route element={<SavedArtists />} path="/savedArtists" /> {/* ✅ nueva */} diff --git a/src/front/js/pages/genre.js b/src/front/js/pages/genre.js new file mode 100644 index 0000000000..cb7a7719e1 --- /dev/null +++ b/src/front/js/pages/genre.js @@ -0,0 +1,69 @@ +import React, { useState } from "react"; +import { useContext } from 'react'; +import { Context } from "../store/appContext"; +import "../../styles/genre.css" + +export const Genre = () => { + + const { store, actions } = useContext(Context); + + // const newArtistFollowed = (item) => { + // actions.followArtist(item) + // } ----- This would be with store/actions + + + const genre = [ + { name: 'Rock' }, + { name: 'Pop' }, + { name: 'Jazz' }, + + ] + + const artist = [ + { name: 'Artist 1', genre: 'Rock', image: 'https://placehold.co/50' }, + { name: 'Artist 2', genre: 'Rock', image: 'https://placehold.co/50' }, + + { name: 'Artist 3', genre: 'Pop', image: 'https://placehold.co/50' }, + { name: 'Artist 4', genre: 'Pop', image: 'https://placehold.co/50' }, + + { name: 'Artist 5', genre: 'Jazz', image: 'https://placehold.co/50' }, + { name: 'Artist 6', genre: 'Jazz', image: 'https://placehold.co/50' }, + // More artists... + ] + + + return ( + <div className="container"> + + {genre?.map((g, index) => { + return ( + <div key={index}> + <h1 className="genretitle mt-3">{g.name}</h1><br /> + <div className="artists row row-cols-1 row-cols-md-3 g-4"> + + {artist?.map((artist, artistIndex) => { + + return ( + <div key={artistIndex} className="artistcard"> + <div className="card"> + <img src={artist.image} className="card-img-top" alt={artist.name} /> + <div className="card-body"> + <h5 className="card-title">{artist.name}</h5> + </div> + <div className="d-flex justify-content-center mb-3"> + <button type="button" className="followbtn btn-outline-purple"> + {/* onClick={() => newArtistFollowed(artist.name)} */} + Follow artist + </button> + </div> + </div> + </div> + ); + })} + </div> + </div> + ); + })} + </div> + ) +} diff --git a/src/front/styles/genre.css b/src/front/styles/genre.css new file mode 100644 index 0000000000..7e3d222293 --- /dev/null +++ b/src/front/styles/genre.css @@ -0,0 +1,37 @@ +.genretitle { + display: flex; + justify-content: start; + color: var(--color-mauveine); +} + +.artists { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + overflow-x: auto; + gap: 16px; + padding: 10px; + scrollbar-width: thin; +} + +.artistcard { + width: 20%; + background-color: rgba(97, 235, 97, 0.641); +} + +.followbtn { + background-color: transparent; + color: var(--color-mauveine); + border: 2px solid var(--color-mauveine); + padding: 10px 20px; + font-size: 16px; + border-radius: 5px; + transition: all 0.3s ease-in-out; + cursor: pointer; +} + +.btn-outline-purple:hover { + background-color: var(--color-mauveine); + color: white; +} + From 81bcd48a184fc14a4ac8aa35f0b260b1b59e2b57 Mon Sep 17 00:00:00 2001 From: Rafael Salinas Gonzalez <rafsalgon@gmail.com> Date: Wed, 26 Feb 2025 19:48:20 +0000 Subject: [PATCH 07/25] Genre view modified --- src/front/js/pages/genre.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/front/js/pages/genre.js b/src/front/js/pages/genre.js index cb7a7719e1..e366466cd7 100644 --- a/src/front/js/pages/genre.js +++ b/src/front/js/pages/genre.js @@ -20,14 +20,14 @@ export const Genre = () => { ] const artist = [ - { name: 'Artist 1', genre: 'Rock', image: 'https://placehold.co/50' }, - { name: 'Artist 2', genre: 'Rock', image: 'https://placehold.co/50' }, + { name: 'Artista 1', genre: 'Rock', image: 'https://placehold.co/50' }, + { name: 'Artista 2', genre: 'Rock', image: 'https://placehold.co/50' }, - { name: 'Artist 3', genre: 'Pop', image: 'https://placehold.co/50' }, - { name: 'Artist 4', genre: 'Pop', image: 'https://placehold.co/50' }, + { name: 'Artista 3', genre: 'Pop', image: 'https://placehold.co/50' }, + { name: 'Artista 4', genre: 'Pop', image: 'https://placehold.co/50' }, - { name: 'Artist 5', genre: 'Jazz', image: 'https://placehold.co/50' }, - { name: 'Artist 6', genre: 'Jazz', image: 'https://placehold.co/50' }, + { name: 'Artista 5', genre: 'Jazz', image: 'https://placehold.co/50' }, + { name: 'Artista 6', genre: 'Jazz', image: 'https://placehold.co/50' }, // More artists... ] @@ -53,7 +53,7 @@ export const Genre = () => { <div className="d-flex justify-content-center mb-3"> <button type="button" className="followbtn btn-outline-purple"> {/* onClick={() => newArtistFollowed(artist.name)} */} - Follow artist + Seguir artista </button> </div> </div> From de770815c8f9e97ae7d9352e5b6a4f305fd6aa47 Mon Sep 17 00:00:00 2001 From: Oscar Parra <oscarparrapalomino@gmail.com> Date: Sun, 2 Mar 2025 03:11:18 +0000 Subject: [PATCH 08/25] cambios en login, creacion user/artist data --- src/front/js/layout.js | 4 + src/front/js/pages/ArtistData.js | 131 +++++++++++++++++++++++++++++++ src/front/js/pages/UserData.js | 131 +++++++++++++++++++++++++++++++ src/front/js/pages/login.js | 2 +- src/front/styles/data.css | 25 ++++++ src/front/styles/login.css | 12 ++- 6 files changed, 297 insertions(+), 8 deletions(-) create mode 100644 src/front/js/pages/ArtistData.js create mode 100644 src/front/js/pages/UserData.js create mode 100644 src/front/styles/data.css diff --git a/src/front/js/layout.js b/src/front/js/layout.js index d135ebefc2..649e521ca0 100755 --- a/src/front/js/layout.js +++ b/src/front/js/layout.js @@ -12,6 +12,8 @@ import { Login } from "./pages/login"; import { UserProfile } from "./pages/UserProfile"; // ✅ Nueva vista import { SavedSongs } from "./pages/SavedSongs"; // ✅ Nueva vista import { SavedArtists } from "./pages/SavedArtists"; // ✅ Nueva vista +import { UserData } from "./pages/UserData" +import { ArtistData } from "./pages/ArtistData" import { Navbar } from "./component/navbar"; import { Footer } from "./component/footer"; @@ -50,6 +52,8 @@ const LayoutContent = () => { <Route element={<UserProfile />} path="/userProfile" /> {/* ✅ nueva */} <Route element={<SavedSongs />} path="/savedSongs" /> {/* ✅ nueva */} <Route element={<SavedArtists />} path="/savedArtists" /> {/* ✅ nueva */} + <Route element={<UserData />} path="/userData" /> + <Route element={<ArtistData />} path="/artistData" /> <Route element={<h1>Not found!</h1>} /> </Routes> diff --git a/src/front/js/pages/ArtistData.js b/src/front/js/pages/ArtistData.js new file mode 100644 index 0000000000..b701e93460 --- /dev/null +++ b/src/front/js/pages/ArtistData.js @@ -0,0 +1,131 @@ +import React, { useState } from "react"; +import "../../styles/data.css" +import Imagen from "../../img/microphone.jpg"; // Asegúrate de que la ruta sea correcta + +export const ArtistData = () => { + const [showEditData, setShowEditData] = useState(false); + + const clickEditData = () => { + setShowEditData(true); + }; + + const cancelEditData = (event) => { + event.preventDefault(); // Prevenir el comportamiento por defecto + setShowEditData(false); + }; + + return ( + <div className="container mt-4"> + <div className="card p-4 d-flex flex-row align-items-center justify-content-between" style={{ maxWidth: "100%", minHeight: "220px" }}> + {/* Información del usuario (Izquierda) */} + <div className="card-body"> + <h1 className="card-title text-white">Nombre del Usuario</h1> + </div> + + {/* Imagen del usuario + Botón (Derecha) */} + <div className="d-flex flex-column align-items-center" style={{ paddingRight: "20px" }}> + <div style={{ width: "140px", height: "140px", overflow: "hidden", borderRadius: "50%" }}> + <img + src={Imagen} + className="img-fluid" + alt="Foto de perfil" + style={{ width: "100%", height: "100%", objectFit: "cover" }} + /> + </div> + <button className=" btn btn-success mt-3 boton" style={{ marginBottom: "10px" }}>Editar Foto</button> + </div> + </div> + + {!showEditData && ( + <div className="card bg-white mt-3"> + <h2 className="card-header">Datos Personales</h2> + <div className="card-body"> + <h4 className="card-title mb-1">Correo</h4> + <p className="card-text mb-3">asdasdasdas@gmail.com</p> + <h4 className="card-title mb-1">Nombre y apellidos</h4> + <p className="card-text mb-3">Pepito diaz</p> + <h4 className="card-title mb-1">Direccion</h4> + <p className="card-text mb-3">Calle de la piruleta 123</p> + <h4 className="card-title mb-1">Telefono</h4> + <p className="card-text mb-3">666666666</p> + <h4 className="card-title mb-1">Artista</h4> + <p className="card-text mb-3">Si</p> + <div className="d-flex justify-content-end"> + <button className="btn btn-success boton mt-3 boton" style={{ marginBottom: "10px" }} onClick={clickEditData}> + Editar Perfil + </button> + </div> + </div> + </div> + )} + + {showEditData && ( + <form className="mt-4 text-white"> + <h1 className="text-center mb-4 text-white">Editar Datos</h1> + <div className="mb-3"> + <label>Email</label> + <input + type="text" + className="form-control form-control-lg" + placeholder="" + required + /> + </div> + <div className="mb-3"> + <label>Nombre de usuario</label> + <input + type="text" + className="form-control form-control-lg" + placeholder="" + required + /> + </div> + <div className="mb-3"> + <label>Contraseña</label> + <input + type="password" + className="form-control form-control-lg" + placeholder="" + required + /> + </div> + <div className="mb-3"> + <label>Repite Contraseña</label> + <input + type="password" + className="form-control form-control-lg" + placeholder="" + required + /> + </div> + <div className="mb-3"> + <label>Dirección</label> + <input + type="text" + className="form-control form-control-lg" + placeholder="" + required + /> + </div> + <div className="mb-3"> + <label>Teléfono</label> + <input + type="number" + className="form-control form-control-lg" + placeholder="" + required + /> + </div> + <div className="d-flex col-md-12 justify-content-center"> + <button type="submit" className="btn btn-danger py-2 col-md-3 me-2"> + Guardar + </button> + <button type="button" className="btn btn-danger py-2 col-md-3 ms-2" onClick={cancelEditData}> + Volver + </button> + </div> + </form> + )} + </div> + ); +}; diff --git a/src/front/js/pages/UserData.js b/src/front/js/pages/UserData.js new file mode 100644 index 0000000000..28cb0892dc --- /dev/null +++ b/src/front/js/pages/UserData.js @@ -0,0 +1,131 @@ +import React, { useState } from "react"; +import "../../styles/data.css" +import Imagen from "../../img/microphone.jpg"; // Asegúrate de que la ruta sea correcta + +export const UserData = () => { + const [showEditData, setShowEditData] = useState(false); + + const clickEditData = () => { + setShowEditData(true); + }; + + const cancelEditData = (event) => { + event.preventDefault(); // Prevenir el comportamiento por defecto + setShowEditData(false); + }; + + return ( + <div className="container mt-4"> + <div className="card p-4 d-flex flex-row align-items-center justify-content-between" style={{ maxWidth: "100%", minHeight: "220px" }}> + {/* Información del usuario (Izquierda) */} + <div className="card-body"> + <h1 className="card-title text-white">Nombre del Usuario</h1> + </div> + + {/* Imagen del usuario + Botón (Derecha) */} + <div className="d-flex flex-column align-items-center" style={{ paddingRight: "20px" }}> + <div style={{ width: "140px", height: "140px", overflow: "hidden", borderRadius: "50%" }}> + <img + src={Imagen} + className="img-fluid" + alt="Foto de perfil" + style={{ width: "100%", height: "100%", objectFit: "cover" }} + /> + </div> + <button className=" boton btn btn-danger mt-3" style={{ marginBottom: "10px" }}>Editar Foto</button> + </div> + </div> + + {!showEditData && ( + <div className="card bg-white mt-3"> + <h2 className="card-header">Datos Personales</h2> + <div className="card-body"> + <h4 className="card-title mb-1">Correo</h4> + <p className="card-text mb-3">asdasdasdas@gmail.com</p> + <h4 className="card-title mb-1">Nombre y apellidos</h4> + <p className="card-text mb-3">Pepito diaz</p> + <h4 className="card-title mb-1">Direccion</h4> + <p className="card-text mb-3">Calle de la piruleta 123</p> + <h4 className="card-title mb-1">Telefono</h4> + <p className="card-text mb-3">666666666</p> + <h4 className="card-title mb-1">Artista</h4> + <p className="card-text mb-3">No</p> + <div className="d-flex justify-content-end"> + <button className="btn btn-danger mt-3" style={{ marginBottom: "10px" }} onClick={clickEditData}> + Editar Perfil + </button> + </div> + </div> + </div> + )} + + {showEditData && ( + <form className="mt-4"> + <h1 className="text-center mb-4 text-white">Editar Datos</h1> + <div className="mb-3"> + <label>Email</label> + <input + type="text" + className="form-control form-control-lg" + placeholder="" + required + /> + </div> + <div className="mb-3"> + <label>Nombre de usuario</label> + <input + type="text" + className="form-control form-control-lg" + placeholder="" + required + /> + </div> + <div className="mb-3"> + <label>Contraseña</label> + <input + type="password" + className="form-control form-control-lg" + placeholder="" + required + /> + </div> + <div className="mb-3"> + <label>Repite Contraseña</label> + <input + type="password" + className="form-control form-control-lg" + placeholder="" + required + /> + </div> + <div className="mb-3"> + <label>Dirección</label> + <input + type="text" + className="form-control form-control-lg" + placeholder="" + required + /> + </div> + <div className="mb-3"> + <label>Teléfono</label> + <input + type="number" + className="form-control form-control-lg" + placeholder="" + required + /> + </div> + <div className="d-flex col-md-12 justify-content-center"> + <button type="submit" className="btn btn-danger py-2 col-md-3 me-2"> + Guardar + </button> + <button type="button" className="btn btn-danger py-2 col-md-3 ms-2" onClick={cancelEditData}> + Volver + </button> + </div> + </form> + )} + </div> + ); +}; diff --git a/src/front/js/pages/login.js b/src/front/js/pages/login.js index 5ea9811bde..5c52ced40f 100644 --- a/src/front/js/pages/login.js +++ b/src/front/js/pages/login.js @@ -29,7 +29,7 @@ export const Login = () => { <div> <h2 className="text-center mb-4">Bienvenido</h2> <button - className="btn btn-primary w-100 mb-3" + className="btn btn-danger boton w-100 mb-3" onClick={() => { setShowLogin(true); setShowRegister(false); diff --git a/src/front/styles/data.css b/src/front/styles/data.css new file mode 100644 index 0000000000..0696b85baa --- /dev/null +++ b/src/front/styles/data.css @@ -0,0 +1,25 @@ +body { + background-color: #0C121C; /* Color oscuro de fondo */ + color: #0C121C;; /* Texto en blanco para mejor visibilidad */ + font-family: Arial, sans-serif; /* Fuente legible */ + margin: 0; + padding: 0; +} + +.card{ + background-color:rgba(163, 40, 163, 0.5); /* 50% de opacidad */ + +} + +/*.boton{ + background: linear-gradient(135deg, #b80000, #ff4e2a, #ff6733, #ff7e3c, #ff914d); +} + +.boton:hover{ + background: linear-gradient(135deg, + rgba(184, 0, 0, 0.75), + rgba(255, 78, 42, 0.75), + rgba(255, 103, 51, 0.75), + rgba(255, 126, 60, 0.75), + rgba(255, 145, 77, 0.75)); +}*/ \ No newline at end of file diff --git a/src/front/styles/login.css b/src/front/styles/login.css index 342737402e..a97e5788eb 100644 --- a/src/front/styles/login.css +++ b/src/front/styles/login.css @@ -1,7 +1,5 @@ -.red { - background: url(../img/microphone.jpg); - background-size: cover; - background-repeat: no-repeat; - margin: 0; - height: 100vh; - } \ No newline at end of file + + + /*.boton{ + background: linear-gradient(135deg, #b80000, #ff4e2a, #ff6733, #ff7e3c, #ff914d); +}*/ \ No newline at end of file From 62b7ab703f51bdca73b7a09c8cdca298d9fd6e84 Mon Sep 17 00:00:00 2001 From: Jaime BP <jaimebernet@gmail.com> Date: Mon, 3 Mar 2025 17:52:01 +0000 Subject: [PATCH 09/25] 1commit --- src/api/routes.py | 21 ++++ src/front/js/component/navbar.js | 10 +- src/front/js/layout.js | 2 + src/front/js/pages/ArtistBio.js | 13 +++ src/front/js/pages/ArtistImages.js | 72 ++++++++++++++ src/front/js/pages/ArtistMusic.js | 39 ++++++++ src/front/js/pages/ArtistProfile.js | 139 ++++++++++++++++++++++++++ src/front/js/pages/ArtistVideos.js | 31 ++++++ src/front/styles/artistProfile.css | 147 ++++++++++++++++++++++++++++ src/front/styles/navbar.css | 33 +++++-- 10 files changed, 494 insertions(+), 13 deletions(-) create mode 100644 src/front/js/pages/ArtistBio.js create mode 100644 src/front/js/pages/ArtistImages.js create mode 100644 src/front/js/pages/ArtistMusic.js create mode 100644 src/front/js/pages/ArtistProfile.js create mode 100644 src/front/js/pages/ArtistVideos.js create mode 100644 src/front/styles/artistProfile.css diff --git a/src/api/routes.py b/src/api/routes.py index 029589a3a1..9e463890c3 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -5,12 +5,26 @@ from api.models import db, User from api.utils import generate_sitemap, APIException from flask_cors import CORS +import os + +# Import the Cloudinary libraries +# ============================== +import cloudinary +from cloudinary import CloudinaryImage +import cloudinary.uploader +import cloudinary.api api = Blueprint('api', __name__) # Allow CORS requests to this API CORS(api) +cloudinary.config( + cloud_name= os.getenv('CLOUD_NAME'), + api_key= os.getenv('CLOUDINARY_API_KEY'), + api_secret= os.getenv('CLOUDINARY_API_SECRET'), + secure= True +) @api.route('/hello', methods=['POST', 'GET']) def handle_hello(): @@ -20,3 +34,10 @@ def handle_hello(): } return jsonify(response_body), 200 + +@api.route('/img', methods=["POST"]) +def upload_image(): + img = request.files["img"] + img_url = cloudinary.uploader.upload(img) + print(img_url) + return jsonify({"img": img_url["url"]}),200 \ No newline at end of file diff --git a/src/front/js/component/navbar.js b/src/front/js/component/navbar.js index 3be878d86e..8837c3fcdc 100755 --- a/src/front/js/component/navbar.js +++ b/src/front/js/component/navbar.js @@ -12,11 +12,12 @@ export const Navbar = () => { return ( <nav className="navbar"> <div className="container"> - <Link to="/" className="navbar-brand">SoundCript</Link> + <Link to="/" className="navbar-brand">SoundCript</Link> - {/* Botón de menú hamburguesa */} - <div className="menu-icon" onClick={toggleMenu}> - ☰ + {/* Icono de usuario (imagen por defecto) */} + <div className="user-menu"> + <div className="user-icon" onClick={toggleMenu}> + <img src="https://cdn-icons-png.flaticon.com/512/3106/3106921.png" alt="Perfil de Usuario" /> </div> {/* Menú desplegable */} @@ -24,6 +25,7 @@ export const Navbar = () => { <Link to="/userProfile" className="dropdown-item" onClick={() => setMenuOpen(false)}>Perfil</Link> <Link to="/logout" className="dropdown-item" onClick={() => setMenuOpen(false)}>Logout</Link> </div> + </div> </div> </nav> ); diff --git a/src/front/js/layout.js b/src/front/js/layout.js index d135ebefc2..fe1d29f2ee 100755 --- a/src/front/js/layout.js +++ b/src/front/js/layout.js @@ -12,6 +12,7 @@ import { Login } from "./pages/login"; import { UserProfile } from "./pages/UserProfile"; // ✅ Nueva vista import { SavedSongs } from "./pages/SavedSongs"; // ✅ Nueva vista import { SavedArtists } from "./pages/SavedArtists"; // ✅ Nueva vista +import ArtistProfile from "./pages/ArtistProfile"; import { Navbar } from "./component/navbar"; import { Footer } from "./component/footer"; @@ -50,6 +51,7 @@ const LayoutContent = () => { <Route element={<UserProfile />} path="/userProfile" /> {/* ✅ nueva */} <Route element={<SavedSongs />} path="/savedSongs" /> {/* ✅ nueva */} <Route element={<SavedArtists />} path="/savedArtists" /> {/* ✅ nueva */} + <Route element={<ArtistProfile />} path="/artist/:artistId" /> {/* ultima */} <Route element={<h1>Not found!</h1>} /> </Routes> diff --git a/src/front/js/pages/ArtistBio.js b/src/front/js/pages/ArtistBio.js new file mode 100644 index 0000000000..a78feb7676 --- /dev/null +++ b/src/front/js/pages/ArtistBio.js @@ -0,0 +1,13 @@ +import React from "react"; + +const ArtistBio = ({ artistData }) => { + return ( + <div> + <h2>Biografía</h2> + <p>{artistData.biography}</p> + {/* Aqui irá el botón para editar la bio si eres el dueño del perfil */} + </div> + ); +}; + +export default ArtistBio; diff --git a/src/front/js/pages/ArtistImages.js b/src/front/js/pages/ArtistImages.js new file mode 100644 index 0000000000..0b27e9bccd --- /dev/null +++ b/src/front/js/pages/ArtistImages.js @@ -0,0 +1,72 @@ +import React, { useState, useEffect } from "react"; + +const ArtistImages = ({ artistData }) => { + const [file, setFile] = useState(null); + const [fileUrl, setFileUrl] = useState(""); + + // Función para actualizar el archivo seleccionado + const handleImgChange = (e) => { + if (e.target.files && e.target.files.length) { + setFile(e.target.files[0]); + } + }; + + // Función para enviar el archivo al backend + const sendFile = async () => { + if (!file) { + alert("El campo de imagen es obligatorio"); + return; + } + try { + const form = new FormData(); + form.append("img", file); + + const response = await fetch(`${process.env.BACKEND_URL}/api/img`, { + method: "POST", + body: form, + }); + console.log(response); + const data = await response.json(); + setFileUrl(data.img); + } catch (error) { + console.error("Error al subir la imagen:", error); + } + }; + + return ( + <div> + <h2>Imágenes</h2> + {artistData.images && artistData.images.length > 0 ? ( + <div className="images-container"> + {artistData.images.map((imgUrl, index) => ( + <img key={index} src={imgUrl} alt={`Imagen ${index}`} /> + ))} + </div> + ) : ( + <p>No hay imágenes disponibles.</p> + )} + {/* Cuadro para subir imágenes */} + <div className="row m-5 bg-secondary bg-opacity-10 p-2"> + <div className="col-12 mb-3"> + <h4 className="m-2">Subir Imagen (Cloudinary)</h4> + <input + type="file" + className="form-control mb-2" + accept="image/jpeg" + onChange={handleImgChange} + /> + <button className="btn btn-primary" onClick={sendFile}> + Send + </button> + </div> + {fileUrl !== "" ? ( + <div className="col-4"> + <img src={fileUrl} className="w-100 h-100" alt="Uploaded" /> + </div> + ) : null} + </div> + </div> + ); +}; + +export default ArtistImages; diff --git a/src/front/js/pages/ArtistMusic.js b/src/front/js/pages/ArtistMusic.js new file mode 100644 index 0000000000..522bca2f48 --- /dev/null +++ b/src/front/js/pages/ArtistMusic.js @@ -0,0 +1,39 @@ +// ArtistMusic.js +import React from "react"; +import "../../styles/artistProfile.css"; + +const ArtistMusic = ({ artistData }) => { + // Aquí podemos implementar la lógica para manejar el "like" + const handleLike = (trackId) => { + console.log("Liked track:", trackId); + // Aquí llamamos a la API o actualizamos el estado + }; + + return ( + <div> + <h2>Música</h2> + {artistData.music && artistData.music.length > 0 ? ( + <ul className="song-list"> + {artistData.music.map((track) => ( + <li key={track.id} className="song-item"> + <span> + <strong>{track.title}</strong> + </span> + <button + className="like-button" + onClick={() => handleLike(track.id)} + > + 👍 Like + </button> + </li> + ))} + </ul> + ) : ( + <p>No hay canciones disponibles.</p> + )} + {/* Aqui irá el botón para subir canciones si eres el dueño del perfil */} + </div> + ); +}; + +export default ArtistMusic; diff --git a/src/front/js/pages/ArtistProfile.js b/src/front/js/pages/ArtistProfile.js new file mode 100644 index 0000000000..047f524733 --- /dev/null +++ b/src/front/js/pages/ArtistProfile.js @@ -0,0 +1,139 @@ +// ArtistProfile.js +import React, { useState, useEffect } from "react"; +import { useParams } from "react-router-dom"; +import ArtistBio from "./ArtistBio"; +import ArtistImages from "./ArtistImages"; +import ArtistVideos from "./ArtistVideos"; +import ArtistMusic from "./ArtistMusic"; +import "../../styles/artistProfile.css"; + +const ArtistProfile = () => { + const { artistId } = useParams(); + const [activeTab, setActiveTab] = useState("bio"); + const [artistData, setArtistData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchArtistData = async () => { + try { + // Simula una llamada a la API + const data = { + id: artistId, + name: "Artist Example", + profilePicture: "https://placehold.co/600x400", + biography: "Esta es la biografía de ejemplo del artista...Esta es la biografía de ejemplo del artista...Esta es la biografía de ejemplo del artista...Esta es la biografía de ejemplo del artista...Esta es la biografía de ejemplo del artista...Esta es la biografía de ejemplo del artista...Esta es la biografía de ejemplo del artista...Esta es la biografía de ejemplo del artista...Esta es la biografía de ejemplo del artista...Esta es la biografía de ejemplo del artista...Esta es la biografía de ejemplo del artista...Esta es la biografía de ejemplo del artista...Esta es la biografía de ejemplo del artista...Esta es la biografía de ejemplo del artista...Esta es la biografía de ejemplo del artista...Esta es la biografía de ejemplo del artista...Esta es la biografía de ejemplo del artista...Esta es la biografía de ejemplo del artista...Esta es la biografía de ejemplo del artista...Esta es la biografía de ejemplo del artista...", + images: ["https://placehold.co/300", "https://placehold.co/300"], + videos: ["https://www.youtube.com/embed/VIDEO_ID"], + music: [ + { id: 1, title: "Canción 1", url: "https://placehold.co/audio1" }, + { id: 2, title: "Canción 2", url: "https://placehold.co/audio2" }, + { id: 3, title: "Canción 1", url: "https://placehold.co/audio1" }, + { id: 4, title: "Canción 1", url: "https://placehold.co/audio1" }, + { id: 5, title: "Canción 1", url: "https://placehold.co/audio1" }, + { id: 6, title: "Canción 1", url: "https://placehold.co/audio1" }, + { id: 7, title: "Canción 1", url: "https://placehold.co/audio1" }, + { id: 8, title: "Canción 1", url: "https://placehold.co/audio1" }, + { id: 9, title: "Canción 1", url: "https://placehold.co/audio1" }, + { id: 10, title: "Canción 1", url: "https://placehold.co/audio1" }, + { id: 11, title: "Canción 1", url: "https://placehold.co/audio1" }, + { id: 12, title: "Canción 1", url: "https://placehold.co/audio1" }, + { id: 13, title: "Canción 1", url: "https://placehold.co/audio1" }, + { id: 14, title: "Canción 1", url: "https://placehold.co/audio1" }, + { id: 15, title: "Canción 1", url: "https://placehold.co/audio1" }, + { id: 16, title: "Canción 1", url: "https://placehold.co/audio1" }, + { id: 17, title: "Canción 1", url: "https://placehold.co/audio1" }, + { id: 18, title: "Canción 1", url: "https://placehold.co/audio1" }, + { id: 19, title: "Canción 1", url: "https://placehold.co/audio1" }, + { id: 20, title: "Canción 1", url: "https://placehold.co/audio1" }, + { id: 21, title: "Canción 1", url: "https://placehold.co/audio1" }, + { id: 22, title: "Canción 1", url: "https://placehold.co/audio1" }, + { id: 23, title: "Canción 1", url: "https://placehold.co/audio1" }, + { id: 24, title: "Canción 1", url: "https://placehold.co/audio1" }, + { id: 25, title: "Canción 1", url: "https://placehold.co/audio1" }, + { id: 26, title: "Canción 1", url: "https://placehold.co/audio1" }, + { id: 27, title: "Canción 1", url: "https://placehold.co/audio1" }, + { id: 28, title: "Canción 1", url: "https://placehold.co/audio1" }, + { id: 29, title: "Canción 1", url: "https://placehold.co/audio1" } + + ] + }; + setArtistData(data); + setLoading(false); + } catch (err) { + setError("Error al cargar los datos del artista."); + setLoading(false); + } + }; + + fetchArtistData(); + }, [artistId]); + + if (loading) return <p>Cargando...</p>; + if (error) return <p>{error}</p>; + + const handleTabChange = (tab) => setActiveTab(tab); + + // Lógica para seguir al artista (ejemplo simple) + const handleFollow = () => { + console.log("Siguiendo al artista:", artistData.id); + // Aquí implementas la lógica de seguir (API, estado, etc.) + }; + + return ( + <div className="artist-profile-container"> + {/* Encabezado del perfil */} + <div className="artist-header"> + <div className="artist-img-container"> + <img + src={artistData.profilePicture} + alt="Artist" + className="artist-profile-picture" + /> + <button className="follow-button" onClick={handleFollow}> + Seguir + </button> + </div> + <h1>{artistData.name}</h1> + </div> + + {/* Menú de pestañas */} + <div className="artist-tabs"> + <button + className={activeTab === "bio" ? "active" : ""} + onClick={() => handleTabChange("bio")} + > + Biografía + </button> + <button + className={activeTab === "images" ? "active" : ""} + onClick={() => handleTabChange("images")} + > + Imágenes + </button> + <button + className={activeTab === "videos" ? "active" : ""} + onClick={() => handleTabChange("videos")} + > + Vídeos + </button> + <button + className={activeTab === "music" ? "active" : ""} + onClick={() => handleTabChange("music")} + > + Música + </button> + </div> + + {/* Contenido de la pestaña activa */} + <div className="artist-content"> + {activeTab === "bio" && <ArtistBio artistData={artistData} />} + {activeTab === "images" && <ArtistImages artistData={artistData} />} + {activeTab === "videos" && <ArtistVideos artistData={artistData} />} + {activeTab === "music" && <ArtistMusic artistData={artistData} />} + </div> + </div> + ); +}; + +export default ArtistProfile; diff --git a/src/front/js/pages/ArtistVideos.js b/src/front/js/pages/ArtistVideos.js new file mode 100644 index 0000000000..c7bb343fc2 --- /dev/null +++ b/src/front/js/pages/ArtistVideos.js @@ -0,0 +1,31 @@ +import React from "react"; + +const ArtistVideos = ({ artistData }) => { + return ( + <div> + <h2>Vídeos</h2> + {artistData.videos && artistData.videos.length > 0 ? ( + <div className="videos-container"> + {artistData.videos.map((videoUrl, index) => ( + <div key={index} className="video-wrapper"> + <iframe + width="560" + height="315" + src={videoUrl} + title={`Video ${index}`} + frameBorder="0" + allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" + allowFullScreen + ></iframe> + </div> + ))} + </div> + ) : ( + <p>No hay vídeos disponibles.</p> + )} + {/* Aqui irá el botón para subir vídeos si eres el dueño del perfil */} + </div> + ); +}; + +export default ArtistVideos; diff --git a/src/front/styles/artistProfile.css b/src/front/styles/artistProfile.css new file mode 100644 index 0000000000..04678a93d9 --- /dev/null +++ b/src/front/styles/artistProfile.css @@ -0,0 +1,147 @@ +.artist-profile-container { + max-width: 800px; + margin: 20px auto; + background-color: var(--color-white); + color: var(--color-black); + border-radius: 10px; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); + padding: 20px; + } + + .artist-header { + display: flex; + align-items: center; + position: relative; + gap: 20px; + margin-bottom: 20px; + } + .artist-header h1 { + margin: 80px; /* Empuja el texto al centro */ + } + + .artist-profile-picture { + width: 150px; + height: 150px; + object-fit: cover; + border-radius: 50%; + } + + .artist-tabs { + display: flex; + gap: 10px; + margin-bottom: 20px; + justify-content: center; + } + + .artist-tabs button { + padding: 10px 20px; + background: var(--color-coquelicot); + color: var(--color-white); + border: none; + border-radius: 5px; + cursor: pointer; + font-weight: bold; + transition: background 0.3s; + } + + .artist-tabs button:hover { + background: var(--color-giants-orange); + } + + .artist-tabs button.active { + background: var(--color-black); + } + + .artist-content { + text-align: center; + } + + .images-container { + display: flex; + flex-wrap: wrap; + gap: 10px; + justify-content: center; + } + + .images-container img { + max-width: 300px; + border-radius: 5px; + } + + .videos-container { + display: flex; + flex-wrap: wrap; + gap: 10px; + justify-content: center; + } + + .video-wrapper { + width: 560px; + max-width: 100%; + margin-bottom: 10px; + } + + ul { + list-style: none; + padding: 0; + } + + li { + background: var(--color-orange-crayola); + margin: 10px 0; + padding: 10px; + border-radius: 5px; + } + + .song-list { + list-style: none; + padding: 0; + } + + .song-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px; + border-bottom: 1px solid var(--color-black); + background: var(--color-orange-crayola); + border-radius: 5px; + margin-bottom: 5px; + } + + .like-button { + background: var(--color-green); + color: var(--color-white); + border: none; + padding: 5px 10px; + border-radius: 5px; + cursor: pointer; + transition: background 0.3s ease; + } + + .like-button:hover { + background: var(--color-mauveine); + } + + + .artist-img-container { + display: flex; + flex-direction: column; + align-items: center; + } + + .follow-button { + margin-top: 10px; + background: var(--color-green); + color: var(--color-white); + border: none; + padding: 5px 10px; + border-radius: 5px; + cursor: pointer; + transition: background 0.3s ease; + } + + .follow-button:hover { + background: var(--color-giants-orange); + } + \ No newline at end of file diff --git a/src/front/styles/navbar.css b/src/front/styles/navbar.css index 873ef3845d..a893ea098b 100644 --- a/src/front/styles/navbar.css +++ b/src/front/styles/navbar.css @@ -1,13 +1,25 @@ .navbar { + margin-top: 15px; display: flex; - justify-content: space-between; align-items: center; - padding: 10px; - background: var(--color-black); + justify-content: space-between; + /* Ejemplo de degradado desde rojo hasta atomic-tangerine */ + background: linear-gradient( + 90deg, + var(--color-red) 0%, + var(--color-giants-orange) 50%, + var(--color-atomic-tangerine) 100% + ); color: var(--color-white); + padding: 10px 20px; + border-radius: 30px; /* Para darle bordes redondeados */ position: relative; } +.user-menu { + position: relative; /* Esto hace que cualquier elemento con posición absoluta se posicione relativo a este contenedor */ +} + .navbar-brand { text-decoration: none; color: var(--color-white); @@ -15,18 +27,21 @@ font-weight: bold; } -/* Menú hamburguesa */ -.menu-icon { - font-size: 2rem; + +.user-icon img { + width: 40px; + height: 40px; + border-radius: 50%; + object-fit: cover; cursor: pointer; - user-select: none; } + /* Menú desplegable */ .dropdown-menu { position: absolute; - top: 50px; - right: 10px; + top: 100%; + right: 1; background: var(--color-white); color: var(--color-black); border-radius: 5px; From 1090fa75c62f617771236ee8c3db6c31f4c0b3b6 Mon Sep 17 00:00:00 2001 From: Rafael Salinas Gonzalez <rafsalgon@gmail.com> Date: Wed, 5 Mar 2025 15:44:05 +0000 Subject: [PATCH 10/25] Load Genres API --- Pipfile | 1 + Pipfile.lock | 130 +++++++++++++++++++++++++-- migrations/versions/22b5bf5541c4_.py | 35 -------- migrations/versions/34a3df02bba3_.py | 35 -------- src/api/admin.py | 3 +- src/api/models.py | 14 +++ src/api/routes.py | 26 +++++- src/front/js/pages/genre.js | 12 ++- src/front/js/store/flux.js | 28 +++++- 9 files changed, 192 insertions(+), 92 deletions(-) delete mode 100644 migrations/versions/22b5bf5541c4_.py delete mode 100644 migrations/versions/34a3df02bba3_.py diff --git a/Pipfile b/Pipfile index b461e2e4ee..78ccb678b4 100644 --- a/Pipfile +++ b/Pipfile @@ -20,6 +20,7 @@ flask-admin = "*" typing-extensions = "*" flask-jwt-extended = "==4.6.0" wtforms = "==3.1.2" +requests = "*" [requires] python_version = "3.10" diff --git a/Pipfile.lock b/Pipfile.lock index 508225b0a1..02f73eb510 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "4f0e9a772f04b621ff0313b7ecfa468af1526aa27df8bfcacac6955d499d352d" + "sha256": "565f98e9600186b666ea99818452df31f2af6834fd198eaf1fcc557fed5d866b" }, "pipfile-spec": 6, "requires": { @@ -18,11 +18,11 @@ "default": { "alembic": { "hashes": [ - "sha256:1acdd7a3a478e208b0503cd73614d5e4c6efafa4e73518bb60e4f2846a37b1c5", - "sha256:496e888245a53adf1498fcab31713a469c65836f8de76e01399aa1c3e90dd213" + "sha256:197de710da4b3e91cf66a826a5b31b5d59a127ab41bd0fc42863e2902ce2bbbe", + "sha256:e1a1c738577bca1f27e68728c910cd389b9a92152ff91d902da649c192e30c49" ], - "markers": "python_version >= '3.8'", - "version": "==1.14.1" + "markers": "python_version >= '3.9'", + "version": "==1.15.1" }, "blinker": { "hashes": [ @@ -40,6 +40,104 @@ "markers": "python_version >= '3.6'", "version": "==2025.1.31" }, + "charset-normalizer": { + "hashes": [ + "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", + "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa", + "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a", + "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", + "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b", + "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", + "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", + "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", + "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", + "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", + "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", + "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", + "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", + "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", + "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", + "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", + "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", + "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", + "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", + "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", + "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e", + "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a", + "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4", + "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca", + "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", + "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", + "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", + "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", + "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", + "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", + "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", + "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", + "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", + "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", + "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", + "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd", + "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c", + "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", + "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", + "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", + "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", + "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824", + "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", + "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf", + "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487", + "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d", + "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd", + "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", + "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534", + "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", + "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", + "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", + "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd", + "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", + "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9", + "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", + "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", + "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d", + "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", + "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", + "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", + "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", + "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", + "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", + "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8", + "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", + "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", + "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", + "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", + "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", + "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", + "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", + "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", + "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", + "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", + "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", + "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", + "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e", + "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6", + "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", + "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", + "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e", + "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", + "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", + "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c", + "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", + "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", + "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089", + "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", + "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e", + "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", + "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616" + ], + "markers": "python_version >= '3.7'", + "version": "==3.4.1" + }, "click": { "hashes": [ "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", @@ -74,11 +172,11 @@ }, "flask-cors": { "hashes": [ - "sha256:5aadb4b950c4e93745034594d9f3ea6591f734bb3662e16e255ffbf5e89c88ef", - "sha256:b9e307d082a9261c100d8fb0ba909eec6a228ed1b60a8315fd85f783d61910bc" + "sha256:6ccb38d16d6b72bbc156c1c3f192bc435bfcc3c2bc864b2df1eb9b2d97b2403c", + "sha256:fa5cb364ead54bbf401a26dbf03030c6b18fb2fcaf70408096a572b409586b0c" ], "index": "pypi", - "version": "==5.0.0" + "version": "==5.0.1" }, "flask-jwt-extended": { "hashes": [ @@ -199,6 +297,14 @@ "index": "pypi", "version": "==23.0.0" }, + "idna": { + "hashes": [ + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" + ], + "markers": "python_version >= '3.6'", + "version": "==3.10" + }, "itsdangerous": { "hashes": [ "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", @@ -447,6 +553,14 @@ "markers": "python_version >= '3.8'", "version": "==6.0.2" }, + "requests": { + "hashes": [ + "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", + "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" + ], + "index": "pypi", + "version": "==2.32.3" + }, "six": { "hashes": [ "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", diff --git a/migrations/versions/22b5bf5541c4_.py b/migrations/versions/22b5bf5541c4_.py deleted file mode 100644 index 0e8edd6c96..0000000000 --- a/migrations/versions/22b5bf5541c4_.py +++ /dev/null @@ -1,35 +0,0 @@ -"""empty message - -Revision ID: 22b5bf5541c4 -Revises: -Create Date: 2025-02-19 19:55:11.728525 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '22b5bf5541c4' -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(length=80), 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/34a3df02bba3_.py b/migrations/versions/34a3df02bba3_.py deleted file mode 100644 index feceaccec3..0000000000 --- a/migrations/versions/34a3df02bba3_.py +++ /dev/null @@ -1,35 +0,0 @@ -"""empty message - -Revision ID: 34a3df02bba3 -Revises: -Create Date: 2025-02-21 09:53:15.839016 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '34a3df02bba3' -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(length=80), 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/src/api/admin.py b/src/api/admin.py index 3eecb64140..1979841fc9 100644 --- a/src/api/admin.py +++ b/src/api/admin.py @@ -1,7 +1,7 @@ import os from flask_admin import Admin -from .models import db, User +from .models import db, User, Genre from flask_admin.contrib.sqla import ModelView def setup_admin(app): @@ -12,6 +12,7 @@ def setup_admin(app): # Add your models here, for example this is how we add a the User model to the admin admin.add_view(ModelView(User, db.session)) + admin.add_view(ModelView(Genre, db.session)) # You can duplicate that line to add mew models # admin.add_view(ModelView(YourModelName, db.session)) \ No newline at end of file diff --git a/src/api/models.py b/src/api/models.py index dccd8421ee..6b41856742 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -16,4 +16,18 @@ def serialize(self): "id": self.id, "email": self.email, # do not serialize the password, its a security breach + } + + # GENRES MODEL TO LOAD GENRES +class Genre(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(250), unique=True, nullable=False) + + def __repr__(self): + return f'<Genre {self.name}>' + + def serialize(self): + return { + "id": self.id, + "name": self.name, } \ No newline at end of file diff --git a/src/api/routes.py b/src/api/routes.py index 9e463890c3..23f4f61e11 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -2,11 +2,11 @@ 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.models import db, User, Genre from api.utils import generate_sitemap, APIException from flask_cors import CORS import os - +import requests # Import the Cloudinary libraries # ============================== import cloudinary @@ -40,4 +40,24 @@ def upload_image(): img = request.files["img"] img_url = cloudinary.uploader.upload(img) print(img_url) - return jsonify({"img": img_url["url"]}),200 \ No newline at end of file + return jsonify({"img": img_url["url"]}),200 + + +# ROUTE TO LOAD GENRES +@api.route('/getGenresapi', methods=["GET"]) +def getGenresApi(): + url = "https://api.deezer.com/genre" + headers = {"accept": "application/json"} + response = requests.get(url, headers = headers) + data = response.json() + for genre in data.get("data", []): + if not Genre.query.filter_by(id = genre.get("id")).first(): + new_genre = Genre(id = genre.get("id"), name = genre.get("name")) + db.session.add(new_genre) + db.session.commit() + return jsonify(data) + +@api.route('/getGenres', methods=["GET"]) +def getGenres(): + genres = Genre.query.filter(Genre.id != 0).all() + return jsonify({"genres": [genre.serialize() for genre in genres]}), 200 \ No newline at end of file diff --git a/src/front/js/pages/genre.js b/src/front/js/pages/genre.js index e366466cd7..6a53e12cc2 100644 --- a/src/front/js/pages/genre.js +++ b/src/front/js/pages/genre.js @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { useContext } from 'react'; import { Context } from "../store/appContext"; import "../../styles/genre.css" @@ -12,12 +12,10 @@ export const Genre = () => { // } ----- This would be with store/actions - const genre = [ - { name: 'Rock' }, - { name: 'Pop' }, - { name: 'Jazz' }, + useEffect(() => { + actions.loadGenres(); // Fetch genres when component mounts + }, []); - ] const artist = [ { name: 'Artista 1', genre: 'Rock', image: 'https://placehold.co/50' }, @@ -35,7 +33,7 @@ export const Genre = () => { return ( <div className="container"> - {genre?.map((g, index) => { + {store.genres?.map((g, index) => { return ( <div key={index}> <h1 className="genretitle mt-3">{g.name}</h1><br /> diff --git a/src/front/js/store/flux.js b/src/front/js/store/flux.js index cc56951a22..0f29132ab7 100755 --- a/src/front/js/store/flux.js +++ b/src/front/js/store/flux.js @@ -15,6 +15,8 @@ const getState = ({ getStore, getActions, setStore }) => { } ] }, + genres: [], + actions: { // Use getActions to call a function within a fuction exampleFunction: () => { @@ -22,14 +24,14 @@ const getState = ({ getStore, getActions, setStore }) => { }, getMessage: async () => { - try{ + try { // fetching data from the backend const resp = await fetch(process.env.BACKEND_URL + "/api/hello") const data = await resp.json() setStore({ message: data.message }) // don't forget to return something, that is how the async resolves return data; - }catch(error){ + } catch (error) { console.log("Error loading message from backend", error) } }, @@ -46,7 +48,27 @@ const getState = ({ getStore, getActions, setStore }) => { //reset the global store setStore({ demo: demo }); - } + }, + + // Load all genres & artists for User Home view + loadGenres: async () => { + try { + const options = { + method: 'GET', headers: { + "Content-Type": "application/json", + }, + }; + const response = await fetch(`${process.env.BACKEND_URL}/api/getGenres`, options) + if (!response.ok) { + console.error("Fetch error loadGenres") + } + const data = await response.json() + setStore({ genres: data["genres"] }) + } + catch (error) { + console.error("Failed to get loadGenres") + } + }, } }; }; From 7c78c57d822974807811f0a20cbd2b2c2cc1e30f Mon Sep 17 00:00:00 2001 From: Rafael Salinas Gonzalez <rafsalgon@gmail.com> Date: Wed, 5 Mar 2025 18:02:34 +0000 Subject: [PATCH 11/25] Models First commit --- src/api/models.py | 180 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 174 insertions(+), 6 deletions(-) diff --git a/src/api/models.py b/src/api/models.py index dccd8421ee..6d734f56d2 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -2,18 +2,186 @@ db = SQLAlchemy() -class User(db.Model): +# class User(db.Model): +# id = db.Column(db.Integer, primary_key=True) +# email = db.Column(db.String(120), unique=True, nullable=False) +# password = db.Column(db.String(80), unique=False, nullable=False) +# is_active = db.Column(db.Boolean(), unique=False, nullable=False) + + + # ADD TO USER TABLE!! + # profile_photo_id = db.Column(db.Integer, db.ForeignKey('Profile_Photo.id')) + # profile_photo = db.relationship('Profile_Photo', backref='user', uselist=False) + + # saved_artist = db.relationship('Saved_Artist', backref='user') + # saved_music = db.relationship('Saved_Music', backref='user') + + +# def __repr__(self): +# return f'<User {self.email}>' + +# def serialize(self): +# return { +# "id": self.id, +# "email": self.email, +# "saved_artist": [saved.serialize() for saved in self.saved_artist], +# "saved_music": [saved.serialize() for saved in self.saved_music], + # do not serialize the password, its a security breach +# } + +# class Artist(db.Model): +# id = db.Column(db.Integer, primary_key=True) +# email = db.Column(db.String(120), unique=True, nullable=False) +# password = db.Column(db.String(80), unique=False, nullable=False) +# is_active = db.Column(db.Boolean(), unique=False, nullable=False) + + + # ADD TO ARTIST TABLE!! + # profile_photo_id = db.Column(db.Integer, db.ForeignKey('Profile_Photo.id')) + # profile_photo = db.relationship('Profile_Photo', backref='artist', uselist=False) + # artist_photos = db.relationship("Photo", backref="artist") + # artist_videos = db.relationship("Video", backref="artist") + # artist_music = db.relationship("Music", backref="artist") + # saved_artist = db.relationship('Saved_Artist', backref='artist') + + +# def __repr__(self): +# return f'<Artist {self.email}>' + +# def serialize(self): +# return { +# "id": self.id, +# "email": self.email, +# # do not serialize the password, its a security breach +# } + + # PROFILE PHOTO CLASS MODEL +class Profile_Photo(db.Model): + __tablename__ = "profile_photo" + id = db.Column(db.Integer, primary_key=True) - email = db.Column(db.String(120), unique=True, nullable=False) - password = db.Column(db.String(80), unique=False, nullable=False) is_active = db.Column(db.Boolean(), unique=False, nullable=False) + user_profile_photo = db.Column(db.String(255), nullable=True, unique=True) # img URL or file path + artist_profile_photo = db.Column(db.String(255), nullable=True, unique=True) # img URL or file path + + entity_type = db.Column(db.String(50), nullable=False) # 'user' or 'artist' + + def __repr__(self): + return f'<ProfilePhoto {self.id, self.user_profile_photo, self.artist_profile_photo}>' + + def serialize(self): + data = { + "id": self.id, + "is_active": self.is_active, + } + if self.user_profile_photo: + data["user_profile_photo"] = self.user_profile_photo + if self.artist_profile_photo: + data["artist_profile_photo"] = self.artist_profile_photo + return data + + # ARTIST POSTS CLASS +class Photo(db.Model): + __tablename__ = "photo" + + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(100), nullable=False) + media_url = db.Column(db.Text, nullable=False, unique=True) # Cloudinary URL + + artist_id = db.Column(db.Integer, db.ForeignKey("artist.id"), nullable=False) + + def __repr__(self): + return f'<Photo {self.title}>' + + def serialize(self): + return { + "id": self.id, + "title": self.title, + "media_url": self.media_url + } + + +class Video(db.Model): + __tablename__ = "video" + + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(100), nullable=False) + media_url = db.Column(db.Text, nullable=False, unique=True) # Cloudinary URL + duration = db.Column(db.Integer, nullable=False) # Duration in seconds + + artist_id = db.Column(db.Integer, db.ForeignKey("artist.id"), nullable=False) + + def __repr__(self): + return f'<Video {self.title}>' + + def serialize(self): + return { + "id": self.id, + "title": self.title, + "media_url": self.media_url, + "duration": self.duration, + } + +class Music(db.Model): + __tablename__ = "music" + + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(200), nullable=False) + media_url = db.Column(db.Text, nullable=False, unique=True) # Cloudinary URL + duration = db.Column(db.Integer, nullable=False) # Duration in seconds + + artist_id = db.Column(db.Integer, db.ForeignKey("artist.id"), nullable=False) + + def __repr__(self): + return f'<Music {self.title}>' + + def serialize(self): + return { + "id": self.id, + "title": self.title, + "media_url": self.media_url, + "duration": self.duration, + } + + # USER SAVED MUSIC / ARTISTS + +class Saved_Artist(db.Model): + __tablename__ = "saved_artist" + + id = db.Column(db.Integer, primary_key=True) + + user_id = db.Column(db.Integer, db.ForeignKey("user.id")) + artist_id = db.Column(db.Integer, db.ForeignKey("artist.id")) + + def __repr__(self): + return f'<Saved_Artist {self.artist_id}>' + + def serialize(self): + return { + "id": self.id, + "user_id": self.user_id, + "artist_id": self.artist_id + } + +class Saved_Music(db.Model): + __tablename__ = "saved_music" + + id = db.Column(db.Integer, primary_key=True) + + user_id = db.Column(db.Integer, db.ForeignKey("user.id")) + artist_id = db.Column(db.Integer, db.ForeignKey("artist.id")) + + music_id = db.Column(db.Integer, db.ForeignKey("music.id")) + + def __repr__(self): - return f'<User {self.email}>' + return f'<Saved_Music {self.music_id}>' def serialize(self): return { "id": self.id, - "email": self.email, - # do not serialize the password, its a security breach + "user_id": self.user_id, + "artist_id": self.artist_id, + "music_id": self.music_id } \ No newline at end of file From 2a1e870c9707a198f9ba189b98183dedb80aeee9 Mon Sep 17 00:00:00 2001 From: Rafael Salinas Gonzalez <rafsalgon@gmail.com> Date: Wed, 5 Mar 2025 20:00:28 +0000 Subject: [PATCH 12/25] Database models Second commit --- src/api/models.py | 164 ++++++++++++++++++++++++++++------------------ 1 file changed, 100 insertions(+), 64 deletions(-) diff --git a/src/api/models.py b/src/api/models.py index 6d734f56d2..5357b2c7db 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -2,86 +2,103 @@ db = SQLAlchemy() -# class User(db.Model): -# id = db.Column(db.Integer, primary_key=True) -# email = db.Column(db.String(120), unique=True, nullable=False) -# password = db.Column(db.String(80), unique=False, nullable=False) -# is_active = db.Column(db.Boolean(), unique=False, nullable=False) - +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(120), unique=True, nullable=False) + password = db.Column(db.String(80), unique=False, nullable=False) + is_active = db.Column(db.Boolean(), unique=False, nullable=False) - # ADD TO USER TABLE!! - # profile_photo_id = db.Column(db.Integer, db.ForeignKey('Profile_Photo.id')) - # profile_photo = db.relationship('Profile_Photo', backref='user', uselist=False) + profile_photo_id = db.Column(db.Integer, db.ForeignKey('Profile_Photo.id')) + profile_photo = db.relationship('Profile_Photo', backref='user', uselist=False) - # saved_artist = db.relationship('Saved_Artist', backref='user') - # saved_music = db.relationship('Saved_Music', backref='user') + saved_artist = db.relationship('Saved_Artist', backref='user') + saved_music = db.relationship('Saved_Music', backref='user') + followed_artists = db.relationship('Follow_Artist', backref='user') -# def __repr__(self): -# return f'<User {self.email}>' + def __repr__(self): + return f'<User {self.email}>' -# def serialize(self): -# return { -# "id": self.id, -# "email": self.email, -# "saved_artist": [saved.serialize() for saved in self.saved_artist], -# "saved_music": [saved.serialize() for saved in self.saved_music], - # do not serialize the password, its a security breach -# } - -# class Artist(db.Model): -# id = db.Column(db.Integer, primary_key=True) -# email = db.Column(db.String(120), unique=True, nullable=False) -# password = db.Column(db.String(80), unique=False, nullable=False) -# is_active = db.Column(db.Boolean(), unique=False, nullable=False) - - - # ADD TO ARTIST TABLE!! - # profile_photo_id = db.Column(db.Integer, db.ForeignKey('Profile_Photo.id')) - # profile_photo = db.relationship('Profile_Photo', backref='artist', uselist=False) - # artist_photos = db.relationship("Photo", backref="artist") - # artist_videos = db.relationship("Video", backref="artist") - # artist_music = db.relationship("Music", backref="artist") - # saved_artist = db.relationship('Saved_Artist', backref='artist') + def serialize(self): + return { + "id": self.id, + "email": self.email, + "saved_artist": [saved.serialize() for saved in self.saved_artist], + "saved_music": [saved.serialize() for saved in self.saved_music], + } +class Artist(db.Model): + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(120), unique=True, nullable=False) + password = db.Column(db.String(80), unique=False, nullable=False) + is_active = db.Column(db.Boolean(), unique=False, nullable=False) + + profile_photo_id = db.Column(db.Integer, db.ForeignKey('Profile_Photo.id')) + profile_photo = db.relationship('Profile_Photo', backref='artist', uselist=False) + + artist_bio = db.relationship("Photo", backref="artist") + artist_photos = db.relationship("Photo", backref="artist") + artist_videos = db.relationship("Video", backref="artist") + artist_music = db.relationship("Music", backref="artist") + + saved_artist = db.relationship('Saved_Artist', backref='artist') -# def __repr__(self): -# return f'<Artist {self.email}>' + followers = db.relationship('Follow_Artist', backref='artist') -# def serialize(self): -# return { -# "id": self.id, -# "email": self.email, -# # do not serialize the password, its a security breach -# } + def __repr__(self): + return f'<Artist {self.email}>' + + def serialize(self): + return { + "id": self.id, + "email": self.email, + } - # PROFILE PHOTO CLASS MODEL + # USER & ARTIST PROFILE PHOTO MODEL class Profile_Photo(db.Model): __tablename__ = "profile_photo" id = db.Column(db.Integer, primary_key=True) is_active = db.Column(db.Boolean(), unique=False, nullable=False) - user_profile_photo = db.Column(db.String(255), nullable=True, unique=True) # img URL or file path - artist_profile_photo = db.Column(db.String(255), nullable=True, unique=True) # img URL or file path + user_id = db.Column(db.Integer, db.ForeignKey("user.id"), unique=True, nullable=True) + artist_id = db.Column(db.Integer, db.ForeignKey("artist.id"), unique=True, nullable=True) - entity_type = db.Column(db.String(50), nullable=False) # 'user' or 'artist' + def __repr__(self): + return f"<ProfilePhoto id={self.id}, user_id={self.user_id}, artist_id={self.artist_id}, profile_photo={self.profile_photo}>" + + def serialize(self): + return { + "id": self.id, + "is_active": self.is_active, + "profile_photo": self.profile_photo, + "user_id": self.user_id, + "artist_id": self.artist_id + } + + # ARTIST BIO MODEL +class Bio(db.Model): + __tablename__ = "bio" + + id = db.Column(db.Integer, primary_key=True) + + text = db.Column(db.String(1000), nullable=True) + + artist_id = db.Column(db.Integer, db.ForeignKey("artist.id"), nullable=False) def __repr__(self): - return f'<ProfilePhoto {self.id, self.user_profile_photo, self.artist_profile_photo}>' + shortened_text = self.text[:50] + '...' if self.text else "No bio available" + return f'<Bio text={shortened_text}>' def serialize(self): - data = { - "id": self.id, - "is_active": self.is_active, + return { + "id": self.id, + "text": self.text, + "artist_id": self.artist_id } - if self.user_profile_photo: - data["user_profile_photo"] = self.user_profile_photo - if self.artist_profile_photo: - data["artist_profile_photo"] = self.artist_profile_photo - return data - # ARTIST POSTS CLASS + + # ARTIST PHOTO MODEL class Photo(db.Model): __tablename__ = "photo" @@ -101,14 +118,14 @@ def serialize(self): "media_url": self.media_url } - + # ARTIST VIDEO MODEL class Video(db.Model): __tablename__ = "video" id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(100), nullable=False) media_url = db.Column(db.Text, nullable=False, unique=True) # Cloudinary URL - duration = db.Column(db.Integer, nullable=False) # Duration in seconds + duration = db.Column(db.Integer, nullable=False) artist_id = db.Column(db.Integer, db.ForeignKey("artist.id"), nullable=False) @@ -123,13 +140,14 @@ def serialize(self): "duration": self.duration, } + # ARTIST MUSIC MODEL class Music(db.Model): __tablename__ = "music" id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(200), nullable=False) media_url = db.Column(db.Text, nullable=False, unique=True) # Cloudinary URL - duration = db.Column(db.Integer, nullable=False) # Duration in seconds + duration = db.Column(db.Integer, nullable=False) artist_id = db.Column(db.Integer, db.ForeignKey("artist.id"), nullable=False) @@ -144,8 +162,8 @@ def serialize(self): "duration": self.duration, } - # USER SAVED MUSIC / ARTISTS + # USER SAVED MUSIC & SAVED ARTISTS MODEL class Saved_Artist(db.Model): __tablename__ = "saved_artist" @@ -171,10 +189,8 @@ class Saved_Music(db.Model): user_id = db.Column(db.Integer, db.ForeignKey("user.id")) artist_id = db.Column(db.Integer, db.ForeignKey("artist.id")) - music_id = db.Column(db.Integer, db.ForeignKey("music.id")) - def __repr__(self): return f'<Saved_Music {self.music_id}>' @@ -184,4 +200,24 @@ def serialize(self): "user_id": self.user_id, "artist_id": self.artist_id, "music_id": self.music_id + } + + + # FOLLOW ARTIST MODEL +class Follow_Artist(db.Model): + __tablename__ = "follow_artist" + + user_id = db.Column(db.Integer, db.ForeignKey("user.id"), primary_key=True) + artist_id = db.Column(db.Integer, db.ForeignKey("artist.id"), primary_key=True) + + is_active = db.Column(db.Boolean, default=True) + + def __repr__(self): + return f'<Follow_Artist user_id={self.user_id}, artist_id={self.artist_id}, is_active={self.is_active}>' + + def serialize(self): + return { + "user_id": self.user_id, + "artist_id": self.artist_id, + "is_active": self.is_active } \ No newline at end of file From a8cfc5a84fad9388ea3e3c53fd179d926507f405 Mon Sep 17 00:00:00 2001 From: Rafael Salinas Gonzalez <rafsalgon@gmail.com> Date: Wed, 5 Mar 2025 20:07:34 +0000 Subject: [PATCH 13/25] Database models done --- src/api/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/api/models.py b/src/api/models.py index 5357b2c7db..6d858a9640 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -23,6 +23,7 @@ def serialize(self): return { "id": self.id, "email": self.email, + # do not serialize the password, its a security breach "saved_artist": [saved.serialize() for saved in self.saved_artist], "saved_music": [saved.serialize() for saved in self.saved_music], } @@ -52,6 +53,7 @@ def serialize(self): return { "id": self.id, "email": self.email, + # do not serialize the password, its a security breach } # USER & ARTIST PROFILE PHOTO MODEL From a69c23d1ba5a620ae3aaef3b002038ec22224601 Mon Sep 17 00:00:00 2001 From: Oscar Parra <oscarparrapalomino@gmail.com> Date: Wed, 5 Mar 2025 20:22:20 +0000 Subject: [PATCH 14/25] creacion de usuarios y login --- Pipfile | 4 +- Pipfile.lock | 38 +-- migrations/versions/22b5bf5541c4_.py | 35 --- migrations/versions/34a3df02bba3_.py | 35 --- src/api/auth.py | 13 + src/api/models.py | 27 +- src/api/routes.py | 52 +++- src/app.py | 3 + src/front/js/pages/login.js | 373 +++++++++++++++++---------- 9 files changed, 359 insertions(+), 221 deletions(-) delete mode 100644 migrations/versions/22b5bf5541c4_.py delete mode 100644 migrations/versions/34a3df02bba3_.py create mode 100644 src/api/auth.py diff --git a/Pipfile b/Pipfile index b461e2e4ee..bc3a6478e3 100644 --- a/Pipfile +++ b/Pipfile @@ -18,8 +18,10 @@ gunicorn = "*" cloudinary = "*" flask-admin = "*" typing-extensions = "*" -flask-jwt-extended = "==4.6.0" wtforms = "==3.1.2" +flask-extended = "*" +flask-jwt-extended = "*" +pyjwt = "==2.9.0" [requires] python_version = "3.10" diff --git a/Pipfile.lock b/Pipfile.lock index 508225b0a1..2b5d1e0627 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "4f0e9a772f04b621ff0313b7ecfa468af1526aa27df8bfcacac6955d499d352d" + "sha256": "df7144212363e2fb107242f0448476de13fa283a57b574ff560e2a499fbc4049" }, "pipfile-spec": 6, "requires": { @@ -18,11 +18,11 @@ "default": { "alembic": { "hashes": [ - "sha256:1acdd7a3a478e208b0503cd73614d5e4c6efafa4e73518bb60e4f2846a37b1c5", - "sha256:496e888245a53adf1498fcab31713a469c65836f8de76e01399aa1c3e90dd213" + "sha256:197de710da4b3e91cf66a826a5b31b5d59a127ab41bd0fc42863e2902ce2bbbe", + "sha256:e1a1c738577bca1f27e68728c910cd389b9a92152ff91d902da649c192e30c49" ], - "markers": "python_version >= '3.8'", - "version": "==1.14.1" + "markers": "python_version >= '3.9'", + "version": "==1.15.1" }, "blinker": { "hashes": [ @@ -74,19 +74,27 @@ }, "flask-cors": { "hashes": [ - "sha256:5aadb4b950c4e93745034594d9f3ea6591f734bb3662e16e255ffbf5e89c88ef", - "sha256:b9e307d082a9261c100d8fb0ba909eec6a228ed1b60a8315fd85f783d61910bc" + "sha256:6ccb38d16d6b72bbc156c1c3f192bc435bfcc3c2bc864b2df1eb9b2d97b2403c", + "sha256:fa5cb364ead54bbf401a26dbf03030c6b18fb2fcaf70408096a572b409586b0c" + ], + "index": "pypi", + "version": "==5.0.1" + }, + "flask-extended": { + "hashes": [ + "sha256:7d4951573da1fb680185970bfb6427934809ddd4a7d372acf67736d4455ae7be", + "sha256:dcf785327209e1967b01e62868f3ff6bc1401a2359746972111969dc16a7fd29" ], "index": "pypi", - "version": "==5.0.0" + "version": "==0.2" }, "flask-jwt-extended": { "hashes": [ - "sha256:63a28fc9731bcc6c4b8815b6f954b5904caa534fc2ae9b93b1d3ef12930dca95", - "sha256:9215d05a9413d3855764bcd67035e75819d23af2fafb6b55197eb5a3313fdfb2" + "sha256:52f35bf0985354d7fb7b876e2eb0e0b141aaff865a22ff6cc33d9a18aa987978", + "sha256:8085d6757505b6f3291a2638c84d207e8f0ad0de662d1f46aa2f77e658a0c976" ], "index": "pypi", - "version": "==4.6.0" + "version": "==4.7.1" }, "flask-migrate": { "hashes": [ @@ -374,11 +382,11 @@ }, "pyjwt": { "hashes": [ - "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", - "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb" + "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850", + "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c" ], - "markers": "python_version >= '3.9'", - "version": "==2.10.1" + "index": "pypi", + "version": "==2.9.0" }, "python-dotenv": { "hashes": [ diff --git a/migrations/versions/22b5bf5541c4_.py b/migrations/versions/22b5bf5541c4_.py deleted file mode 100644 index 0e8edd6c96..0000000000 --- a/migrations/versions/22b5bf5541c4_.py +++ /dev/null @@ -1,35 +0,0 @@ -"""empty message - -Revision ID: 22b5bf5541c4 -Revises: -Create Date: 2025-02-19 19:55:11.728525 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '22b5bf5541c4' -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(length=80), 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/34a3df02bba3_.py b/migrations/versions/34a3df02bba3_.py deleted file mode 100644 index feceaccec3..0000000000 --- a/migrations/versions/34a3df02bba3_.py +++ /dev/null @@ -1,35 +0,0 @@ -"""empty message - -Revision ID: 34a3df02bba3 -Revises: -Create Date: 2025-02-21 09:53:15.839016 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '34a3df02bba3' -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(length=80), 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/src/api/auth.py b/src/api/auth.py new file mode 100644 index 0000000000..e33936016b --- /dev/null +++ b/src/api/auth.py @@ -0,0 +1,13 @@ +from flask_jwt_extended import JWTManager +from api.models import User + +jwt = JWTManager() + +@jwt.user_identity_loader +def user_identity_lookup(user): + return user.id + +@jwt.user_lookup_loader +def user_lookuo_callback(_jwtheader, jwt_data): + identity = jwt_data["sub"] + return User.query.filter_by(id=identity).one_or_none() \ No newline at end of file diff --git a/src/api/models.py b/src/api/models.py index dccd8421ee..6e5e2956d1 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -1,19 +1,40 @@ from flask_sqlalchemy import SQLAlchemy +from werkzeug.security import generate_password_hash, check_password_hash db = SQLAlchemy() class User(db.Model): id = db.Column(db.Integer, primary_key=True) + fullName = db.Column(db.String(120), unique=True, nullable=False) + username = db.Column(db.String(120), unique=True, nullable=False) + address = db.Column(db.String(120), unique=True, nullable=False) email = db.Column(db.String(120), unique=True, nullable=False) - password = db.Column(db.String(80), unique=False, nullable=False) - is_active = db.Column(db.Boolean(), unique=False, nullable=False) + password_hash = db.Column(db.String(512), unique=False, nullable=False) + is_artist = db.Column(db.Boolean(), default=False) + is_active = db.Column(db.Boolean(), default=True) def __repr__(self): return f'<User {self.email}>' + + def artist(is_artist): + if is_artist: + return "Si" + else: + return "No" def serialize(self): return { "id": self.id, + "fullName": self.fullName, + "username": self.username, "email": self.email, + "address":self.address, + "artist":self.artist() # do not serialize the password, its a security breach - } \ No newline at end of file + } + + def set_password(self, password): + self.password_hash = generate_password_hash(password) + + def check_password(self,password): + return check_password_hash(self.password_hash,password) \ No newline at end of file diff --git a/src/api/routes.py b/src/api/routes.py index 9e463890c3..b1c028e7cb 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -7,6 +7,8 @@ from flask_cors import CORS import os +from flask_jwt_extended import create_access_token, current_user, jwt_required + # Import the Cloudinary libraries # ============================== import cloudinary @@ -40,4 +42,52 @@ def upload_image(): img = request.files["img"] img_url = cloudinary.uploader.upload(img) print(img_url) - return jsonify({"img": img_url["url"]}),200 \ No newline at end of file + return jsonify({"img": img_url["url"]}),200 + + +# Creacion de usuario +@api.route('/register', methods=['POST']) +def register(): + fullName = request.json.get('fullName', None) + username = request.json.get('username', None) + address = request.json.get('address', None) + email = request.json.get('email', None) + password = request.json.get('password', None) + + if not fullName or not username or not email or not password: + return jsonify({"msg": "Missing required fields"}), 400 + + existing_user = User.query.filter_by(email=email).first() + if existing_user: + return jsonify({"msg": "Email already exists"}), 400 + + existing_user = User.query.filter_by(username=username).first() + if existing_user: + return jsonify({"msg": "Username already exists"}), 400 + + user = User(fullName=fullName, username=username, address=address, email=email, is_artist=True, is_active=True) + user.set_password(password) + + db.session.add(user) + db.session.commit() + + return jsonify({"msg": "User has been created"}), 201 + +@api.route('/login', methods=['POST']) +def generate_token(): + username = request.json.get("username", None) + password = request.json.get("password", None) + + user = User.query.filter_by(username=username).one_or_none() + + if not user or not user.check_password(password): + return jsonify("Wrong username or password"), 401 + + access_token = create_access_token(identity=user) # Se pasa user.id + return jsonify(access_token=access_token) + + +@api.route('/profile', methods=['GET']) +@jwt_required() +def get_current_user(): + return jsonify(current_user.serialize()), 200 diff --git a/src/app.py b/src/app.py index 0ea8351d5f..2ad028d6a2 100644 --- a/src/app.py +++ b/src/app.py @@ -10,6 +10,7 @@ from api.routes import api from api.admin import setup_admin from api.commands import setup_commands +from api.auth import jwt # from models import Person @@ -42,6 +43,8 @@ # Handle/serialize errors like a JSON object +#inicializo el jwt con mi app +jwt.init_app(app) @app.errorhandler(APIException) def handle_invalid_usage(error): diff --git a/src/front/js/pages/login.js b/src/front/js/pages/login.js index 5c52ced40f..043755db23 100644 --- a/src/front/js/pages/login.js +++ b/src/front/js/pages/login.js @@ -8,6 +8,92 @@ export const Login = () => { const [showRegister, setShowRegister] = useState(false); const [showForgoten, setShowForgoten] = useState(false); + //Estado para almacenar datos del formulario de registro + + const[formulario, setFormulario] = useState({ + fullName:"", + username:"", + email:"", + address:"", + password:"", + confirmPassword:"", + }); + + // Estados para manejar el login + const [loginData, setLoginData] = useState({ + username:"", + password:"", + }); + + // Manejar los cambios en los inputs del registro + const handleChange = (e) => { + setFormulario({...formulario, [e.target.name]: e.target.value}); + }; + + // Manejar cambios en los inpusts del login + const handleLoginChange = (e) => { + console.log("cambir campos: ", e.target.name, "Nuevo valor:", e.target.value); + setLoginData({...loginData, [e.target.name]:e.target.value}); + }; + + // Enviar formulario de registro + const handleRegister = async (e) =>{ + e.preventDefault(); + + if (formulario.password !== formulario.confirmPassword){ + alert("Las contraseñas no coinciden"); + return; + } + + console.log("Datos del formulario de registro:", formulario); // Verifica que los datos sean correctos antes de enviarlos + + try{ + const response = await fetch("https://fluffy-chainsaw-v6qq999gg9xwfx99v-3001.app.github.dev/api/register",{ + method:"POST", + headers:{"Content-type":"application/json"}, + body:JSON.stringify(formulario), + }); + + const data = await response.json(); + + if(response.ok){ + alert("Usuario registrado con exito"); + setShowRegister(false); + setShowLogin(true); + }else{ + alert(data.message || "Error al registrar usuario") + } + } catch(error){ + alert("Error en el servidor") + } + }; + + // Enviar formulario de inicio de sesion + const handleLogin = async (e) =>{ + e.preventDefault(); + + try{ + const response = await fetch("https://fluffy-chainsaw-v6qq999gg9xwfx99v-3001.app.github.dev/login", { + method:"POST", + headers:{"Content-Type":"application/json"}, + body: JSON.stringify(loginData) + }); + + const data = await response.json(); + + if(response.ok){ + alert("Inicio de sesion correcto"); + localStorage.setItem("Token", data.token); // Guarda el token en local + localStorage.setItem("user", JSON.stringify(data.user)) // guarda los datos del usuario + // aqui puedes redirir a otra pagina + } else{ + alert(data.message || "Error al iniciar sesion") + } + }catch (error){ + alert("Error en el servidor") + } + } + return ( <div style={{ @@ -52,146 +138,171 @@ export const Login = () => { {/* Formulario de inicio de sesión */} {showLogin && !showForgoten && ( - <form> - <h2 className="text-center mb-4">Iniciar sesión</h2> - <div className="mb-3"> - <input - type="text" - className="form-control form-control-lg" - placeholder="Nombre de usuario" - required - /> - </div> - <div className="mb-3"> - <input - type="password" - className="form-control form-control-lg" - placeholder="Contraseña" - required - /> - </div> - <button type="submit" className="btn btn-danger w-100 py-2"> - Ingresar - </button> - <div className="text-center mt-2"> - <a href="#" className="text-decoration-none" onClick={() => - setShowForgoten(true) - }>¿He olvidado mi contraseña?</a> - <button - type="button" - className="btn btn-link w-100 mt-2" - onClick={() => { - setShowForgoten(false); - setShowLogin(false); - setShowRegister(false) - }} - > - Volver - </button> - </div> - </form> - )} - - {/* Formulario de registro */} - {showRegister && ( - <form> - <h2 className="text-center mb-4">Registro</h2> - <div className="mb-3"> - <input - type="text" - className="form-control form-control-lg" - placeholder="Nombre y apellidos" - required - /> - </div> - <div className="mb-3"> - <input - type="text" - className="form-control form-control-lg" - placeholder="Nombre de usuario" - required - /> - </div> - <div className="mb-3"> - <input - type="email" - className="form-control form-control-lg" - placeholder="Correo electrónico" - required - /> - </div> - <div className="mb-3"> - <input - type="text" - className="form-control form-control-lg" - placeholder="Dirección" - required - /> - </div> - <div className="mb-3"> - <input - type="password" - className="form-control form-control-lg" - placeholder="Contraseña" - required - /> - </div> - <div className="mb-3"> - <input - type="password" - className="form-control form-control-lg" - placeholder="Confirmar contraseña" - required - /> - </div> - <div className="mb-3"> - <label className="form-label">Subir foto de perfil</label> - <input type="file" className="form-control form-control-lg" /> - </div> - <button type="submit" className="btn btn-success w-100 py-2"> - Registrarse - </button> + <form onSubmit={handleLogin}> + <h2 className="text-center mb-4">Iniciar sesión</h2> + <div className="mb-3"> + <input + type="text" + className="form-control form-control-lg" + placeholder="Nombre de usuario" + name = "username" + value={loginData.username} + onChange={handleLoginChange} + required + /> + </div> + <div className="mb-3"> + <input + type="password" + className="form-control form-control-lg" + placeholder="Contraseña" + name = "password" + value={loginData.password} + onChange={handleLoginChange} + required + /> + </div> + <button type="submit" className="btn btn-danger w-100 py-2"> + Ingresar + </button> + <div className="text-center mt-2"> + <a href="#" className="text-decoration-none" onClick={() => + setShowForgoten(true) + }>¿He olvidado mi contraseña?</a> <button type="button" className="btn btn-link w-100 mt-2" - onClick={() => setShowRegister(false)} + onClick={() => { + setShowForgoten(false); + setShowLogin(false); + setShowRegister(false) + }} > Volver </button> - </form> + </div> + </form > + )} + + {/* Formulario de registro */} + {showRegister && ( + <form onSubmit={handleRegister}> + <h2 className="text-center mb-4">Registro</h2> + <div className="mb-3"> + <input + type="text" + className="form-control form-control-lg" + placeholder="Nombre y apellidos" + name="fullName" + value={formulario.fullName} + onChange={handleChange} + required + /> + </div> + <div className="mb-3"> + <input + type="text" + className="form-control form-control-lg" + placeholder="Nombre de usuario" + name="username" + value={formulario.username} + onChange={handleChange} + required + /> + </div> + <div className="mb-3"> + <input + type="email" + className="form-control form-control-lg" + placeholder="Correo electrónico" + name = "email" + value={formulario.email} + onChange={handleChange} + required + /> + </div> + <div className="mb-3"> + <input + type="text" + className="form-control form-control-lg" + placeholder="Dirección" + name = "address" + value={formulario.address} + onChange={handleChange} + required + /> + </div> + <div className="mb-3"> + <input + type="password" + className="form-control form-control-lg" + placeholder="Contraseña" + name = "password" + value={formulario.password} + onChange={handleChange} + required + /> + </div> + <div className="mb-3"> + <input + type="password" + className="form-control form-control-lg" + placeholder="Confirmar contraseña" + name = "confirmPassword" + value={formulario.confirmPassword} + onChange={handleChange} + required + /> + </div> + <div className="mb-3"> + <label className="form-label">Subir foto de perfil</label> + <input type="file" className="form-control form-control-lg" /> + </div> + <button type="submit" className="btn btn-success w-100 py-2"> + Registrarse + </button> + <button + type="button" + className="btn btn-link w-100 mt-2" + onClick={() => setShowRegister(false)} + > + Volver + </button> + </form> )} {/* Formulario de recuperar contraseña*/} {showForgoten && ( - <form> - <h2 className="text-center mb-4">¿He olvidado mi contraseña?</h2> - <div className="mb-3"> - <input - type="text" - className="form-control form-control-lg" - placeholder="Nomber de usuario" - required - /> - </div> - <div className="mb-3"> - <input - type="password" - className="form-control form-control-lg" - placeholder="Nueva contraseña" - required - /> - </div> - <button - type="button" - className="btn btn-link w-100 mt-2" - onClick={() => { - setShowLogin(true); - setShowRegister(false); - setShowForgoten(false) - }} - > - Volver - </button> - </form>)} + <form onSubmit={handleRegister}> + <h2 className="text-center mb-4">¿He olvidado mi contraseña?</h2> + <div className="mb-3"> + <input + type="text" + className="form-control form-control-lg" + placeholder="Nomber de usuario" + required + /> + </div> + <div className="mb-3"> + <input + type="password" + className="form-control form-control-lg" + placeholder="Nueva contraseña" + required + /> + </div> + <button + type="button" + className="btn btn-link w-100 mt-2" + onClick={() => { + setShowLogin(true); + setShowRegister(false); + setShowForgoten(false) + }} + > + Volver + </button> + </form> + )} </div> From 9db4a6f8b92a609ed38ffec9a9f6ff922d287830 Mon Sep 17 00:00:00 2001 From: Rafael Salinas Gonzalez <rafsalgon@gmail.com> Date: Wed, 5 Mar 2025 21:08:07 +0000 Subject: [PATCH 15/25] Database Models BACKEND done after reset migrations --- migrations/versions/22b5bf5541c4_.py | 35 ---------------------------- migrations/versions/34a3df02bba3_.py | 35 ---------------------------- src/api/models.py | 23 +++++++++--------- 3 files changed, 11 insertions(+), 82 deletions(-) delete mode 100644 migrations/versions/22b5bf5541c4_.py delete mode 100644 migrations/versions/34a3df02bba3_.py diff --git a/migrations/versions/22b5bf5541c4_.py b/migrations/versions/22b5bf5541c4_.py deleted file mode 100644 index 0e8edd6c96..0000000000 --- a/migrations/versions/22b5bf5541c4_.py +++ /dev/null @@ -1,35 +0,0 @@ -"""empty message - -Revision ID: 22b5bf5541c4 -Revises: -Create Date: 2025-02-19 19:55:11.728525 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '22b5bf5541c4' -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(length=80), 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/34a3df02bba3_.py b/migrations/versions/34a3df02bba3_.py deleted file mode 100644 index feceaccec3..0000000000 --- a/migrations/versions/34a3df02bba3_.py +++ /dev/null @@ -1,35 +0,0 @@ -"""empty message - -Revision ID: 34a3df02bba3 -Revises: -Create Date: 2025-02-21 09:53:15.839016 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '34a3df02bba3' -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(length=80), 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/src/api/models.py b/src/api/models.py index 6d858a9640..944f222225 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -3,13 +3,14 @@ db = SQLAlchemy() class User(db.Model): + id = db.Column(db.Integer, primary_key=True) email = db.Column(db.String(120), unique=True, nullable=False) password = db.Column(db.String(80), unique=False, nullable=False) is_active = db.Column(db.Boolean(), unique=False, nullable=False) - profile_photo_id = db.Column(db.Integer, db.ForeignKey('Profile_Photo.id')) - profile_photo = db.relationship('Profile_Photo', backref='user', uselist=False) + profile_photo_id = db.Column(db.Integer, db.ForeignKey('profile_photo.id'), unique=True) + profile_photo = db.relationship('Profile_Photo', uselist=False, foreign_keys=[profile_photo_id]) saved_artist = db.relationship('Saved_Artist', backref='user') saved_music = db.relationship('Saved_Music', backref='user') @@ -29,15 +30,16 @@ def serialize(self): } class Artist(db.Model): + id = db.Column(db.Integer, primary_key=True) email = db.Column(db.String(120), unique=True, nullable=False) password = db.Column(db.String(80), unique=False, nullable=False) is_active = db.Column(db.Boolean(), unique=False, nullable=False) - profile_photo_id = db.Column(db.Integer, db.ForeignKey('Profile_Photo.id')) - profile_photo = db.relationship('Profile_Photo', backref='artist', uselist=False) + profile_photo_id = db.Column(db.Integer, db.ForeignKey('profile_photo.id'), unique=True) + profile_photo = db.relationship('Profile_Photo', uselist=False, foreign_keys=[profile_photo_id]) - artist_bio = db.relationship("Photo", backref="artist") + artist_bio = db.relationship("Bio", backref="artist") artist_photos = db.relationship("Photo", backref="artist") artist_videos = db.relationship("Video", backref="artist") artist_music = db.relationship("Music", backref="artist") @@ -61,21 +63,18 @@ class Profile_Photo(db.Model): __tablename__ = "profile_photo" id = db.Column(db.Integer, primary_key=True) - is_active = db.Column(db.Boolean(), unique=False, nullable=False) - user_id = db.Column(db.Integer, db.ForeignKey("user.id"), unique=True, nullable=True) - artist_id = db.Column(db.Integer, db.ForeignKey("artist.id"), unique=True, nullable=True) + media_url = db.Column(db.String(255), nullable=False) + is_active = db.Column(db.Boolean(), unique=False, nullable=False) def __repr__(self): - return f"<ProfilePhoto id={self.id}, user_id={self.user_id}, artist_id={self.artist_id}, profile_photo={self.profile_photo}>" + return f"<Profile_Photo {self.id}>" def serialize(self): return { "id": self.id, + "media_url": self.media_url, "is_active": self.is_active, - "profile_photo": self.profile_photo, - "user_id": self.user_id, - "artist_id": self.artist_id } # ARTIST BIO MODEL From 73653fc312130e89a65efc17d62138d4dcfb61a3 Mon Sep 17 00:00:00 2001 From: Rafael Salinas Gonzalez <rafsalgon@gmail.com> Date: Thu, 6 Mar 2025 10:26:15 +0000 Subject: [PATCH 16/25] Database models Final commit --- src/api/models.py | 130 +++++++++++++--------------------------------- 1 file changed, 37 insertions(+), 93 deletions(-) diff --git a/src/api/models.py b/src/api/models.py index 944f222225..bffd963213 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -2,111 +2,74 @@ db = SQLAlchemy() + # USER REGISTER AND PROFILE MODEL class User(db.Model): + __tablename__ = "user" id = db.Column(db.Integer, primary_key=True) + + full_name = db.Column(db.String(120), unique=True, nullable=True) + username = db.Column(db.String(120), unique=True, nullable=True) + email = db.Column(db.String(120), unique=True, nullable=False) password = db.Column(db.String(80), unique=False, nullable=False) - is_active = db.Column(db.Boolean(), unique=False, nullable=False) - profile_photo_id = db.Column(db.Integer, db.ForeignKey('profile_photo.id'), unique=True) - profile_photo = db.relationship('Profile_Photo', uselist=False, foreign_keys=[profile_photo_id]) + is_artist = db.Column(db.Boolean, default=False) # Check if account is artist + profile_photo = db.Column(db.String(255), nullable=True) # Profile photo URL - saved_artist = db.relationship('Saved_Artist', backref='user') + # Relationships + followed_artists = db.relationship('Follow_Artist', backref='follow_artist') saved_music = db.relationship('Saved_Music', backref='user') - followed_artists = db.relationship('Follow_Artist', backref='user') - def __repr__(self): - return f'<User {self.email}>' + return f'<User {self.username}>' def serialize(self): return { "id": self.id, + "full_name": self.full_name, + "username": self.username, "email": self.email, - # do not serialize the password, its a security breach - "saved_artist": [saved.serialize() for saved in self.saved_artist], - "saved_music": [saved.serialize() for saved in self.saved_music], + "is_artist": self.is_artist, + "profile_photo": self.profile_photo, } - -class Artist(db.Model): + + # ARTIST PROFILE MODEL +class Artist_Profile(db.Model): + __tablename__ = "artist_profile" id = db.Column(db.Integer, primary_key=True) - email = db.Column(db.String(120), unique=True, nullable=False) - password = db.Column(db.String(80), unique=False, nullable=False) - is_active = db.Column(db.Boolean(), unique=False, nullable=False) + bio = db.Column(db.Text, nullable=True) - profile_photo_id = db.Column(db.Integer, db.ForeignKey('profile_photo.id'), unique=True) - profile_photo = db.relationship('Profile_Photo', uselist=False, foreign_keys=[profile_photo_id]) + # Relationships + artist_id = db.Column(db.Integer, db.ForeignKey('users.id'), unique=True, nullable=False) - artist_bio = db.relationship("Bio", backref="artist") artist_photos = db.relationship("Photo", backref="artist") artist_videos = db.relationship("Video", backref="artist") artist_music = db.relationship("Music", backref="artist") - saved_artist = db.relationship('Saved_Artist', backref='artist') - - followers = db.relationship('Follow_Artist', backref='artist') - def __repr__(self): - return f'<Artist {self.email}>' + return f'<Artist_Profile {self.id}>' def serialize(self): return { "id": self.id, - "email": self.email, - # do not serialize the password, its a security breach + "artist_id": self.artist_id, + "bio": self.bio, + "artist_photos": self.artist_photos, + "artist_videos": self.artist_videos, + "artist_music": self.artist_music } - # USER & ARTIST PROFILE PHOTO MODEL -class Profile_Photo(db.Model): - __tablename__ = "profile_photo" - - id = db.Column(db.Integer, primary_key=True) - - media_url = db.Column(db.String(255), nullable=False) - is_active = db.Column(db.Boolean(), unique=False, nullable=False) - - def __repr__(self): - return f"<Profile_Photo {self.id}>" - - def serialize(self): - return { - "id": self.id, - "media_url": self.media_url, - "is_active": self.is_active, - } - - # ARTIST BIO MODEL -class Bio(db.Model): - __tablename__ = "bio" - - id = db.Column(db.Integer, primary_key=True) - - text = db.Column(db.String(1000), nullable=True) - - artist_id = db.Column(db.Integer, db.ForeignKey("artist.id"), nullable=False) - - def __repr__(self): - shortened_text = self.text[:50] + '...' if self.text else "No bio available" - return f'<Bio text={shortened_text}>' - - def serialize(self): - return { - "id": self.id, - "text": self.text, - "artist_id": self.artist_id - } - - # ARTIST PHOTO MODEL class Photo(db.Model): __tablename__ = "photo" id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(100), nullable=False) - media_url = db.Column(db.Text, nullable=False, unique=True) # Cloudinary URL + media_url = db.Column(db.Text, nullable=False) # Cloudinary URL + # Relationships artist_id = db.Column(db.Integer, db.ForeignKey("artist.id"), nullable=False) def __repr__(self): @@ -119,15 +82,17 @@ def serialize(self): "media_url": self.media_url } + # ARTIST VIDEO MODEL class Video(db.Model): __tablename__ = "video" id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(100), nullable=False) - media_url = db.Column(db.Text, nullable=False, unique=True) # Cloudinary URL + media_url = db.Column(db.Text, nullable=False) # Cloudinary URL duration = db.Column(db.Integer, nullable=False) + # Relationships artist_id = db.Column(db.Integer, db.ForeignKey("artist.id"), nullable=False) def __repr__(self): @@ -147,9 +112,10 @@ class Music(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(200), nullable=False) - media_url = db.Column(db.Text, nullable=False, unique=True) # Cloudinary URL + media_url = db.Column(db.Text, nullable=False) # Cloudinary URL duration = db.Column(db.Integer, nullable=False) + # Relationships artist_id = db.Column(db.Integer, db.ForeignKey("artist.id"), nullable=False) def __repr__(self): @@ -164,32 +130,14 @@ def serialize(self): } - # USER SAVED MUSIC & SAVED ARTISTS MODEL -class Saved_Artist(db.Model): - __tablename__ = "saved_artist" - - id = db.Column(db.Integer, primary_key=True) - - user_id = db.Column(db.Integer, db.ForeignKey("user.id")) - artist_id = db.Column(db.Integer, db.ForeignKey("artist.id")) - - def __repr__(self): - return f'<Saved_Artist {self.artist_id}>' - - def serialize(self): - return { - "id": self.id, - "user_id": self.user_id, - "artist_id": self.artist_id - } - + # USER SAVED MUSIC & FOLLOW ARTIST MODEL class Saved_Music(db.Model): __tablename__ = "saved_music" id = db.Column(db.Integer, primary_key=True) + # Relationships user_id = db.Column(db.Integer, db.ForeignKey("user.id")) - artist_id = db.Column(db.Integer, db.ForeignKey("artist.id")) music_id = db.Column(db.Integer, db.ForeignKey("music.id")) def __repr__(self): @@ -199,7 +147,6 @@ def serialize(self): return { "id": self.id, "user_id": self.user_id, - "artist_id": self.artist_id, "music_id": self.music_id } @@ -211,8 +158,6 @@ class Follow_Artist(db.Model): user_id = db.Column(db.Integer, db.ForeignKey("user.id"), primary_key=True) artist_id = db.Column(db.Integer, db.ForeignKey("artist.id"), primary_key=True) - is_active = db.Column(db.Boolean, default=True) - def __repr__(self): return f'<Follow_Artist user_id={self.user_id}, artist_id={self.artist_id}, is_active={self.is_active}>' @@ -220,5 +165,4 @@ def serialize(self): return { "user_id": self.user_id, "artist_id": self.artist_id, - "is_active": self.is_active } \ No newline at end of file From 54984ce435f1714ea636ddeabdd77afc7c5258fb Mon Sep 17 00:00:00 2001 From: Rafael Salinas Gonzalez <rafsalgon@gmail.com> Date: Thu, 6 Mar 2025 10:55:55 +0000 Subject: [PATCH 17/25] Database Model user modified --- src/api/models.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/api/models.py b/src/api/models.py index bffd963213..a3c186c5cb 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -1,5 +1,6 @@ from flask_sqlalchemy import SQLAlchemy + db = SQLAlchemy() # USER REGISTER AND PROFILE MODEL @@ -8,13 +9,15 @@ class User(db.Model): id = db.Column(db.Integer, primary_key=True) - full_name = db.Column(db.String(120), unique=True, nullable=True) + fullName = db.Column(db.String(120), unique=True, nullable=True) username = db.Column(db.String(120), unique=True, nullable=True) + address = db.Column(db.String(120), unique=True, nullable=False) email = db.Column(db.String(120), unique=True, nullable=False) - password = db.Column(db.String(80), unique=False, nullable=False) + password_hash = db.Column(db.String(512), unique=False, nullable=False) is_artist = db.Column(db.Boolean, default=False) # Check if account is artist + is_active = db.Column(db.Boolean(), default=True) profile_photo = db.Column(db.String(255), nullable=True) # Profile photo URL # Relationships @@ -23,17 +26,31 @@ class User(db.Model): def __repr__(self): return f'<User {self.username}>' + + def artist(is_artist): + if is_artist: + return "Si" + else: + return "No" def serialize(self): return { "id": self.id, - "full_name": self.full_name, + "full_name": self.fullName, "username": self.username, "email": self.email, - "is_artist": self.is_artist, + "address":self.address, + "artist":self.artist(), "profile_photo": self.profile_photo, } +def set_password(self, password): + self.password_hash = generate_password_hash(password) + +def check_password(self,password): + return check_password_hash(self.password_hash,password) + + # ARTIST PROFILE MODEL class Artist_Profile(db.Model): __tablename__ = "artist_profile" From 911fbfc7f6d7f12499b7f8363f1e03cc1e8671ad Mon Sep 17 00:00:00 2001 From: Rafael Salinas Gonzalez <rafsalgon@gmail.com> Date: Thu, 6 Mar 2025 14:14:05 +0000 Subject: [PATCH 18/25] Last commit I promise --- src/api/models.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/api/models.py b/src/api/models.py index 9e53b9a66b..716924a36c 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -60,11 +60,11 @@ class Artist_Profile(db.Model): bio = db.Column(db.Text, nullable=True) # Relationships - artist_id = db.Column(db.Integer, db.ForeignKey('users.id'), unique=True, nullable=False) + artist_id = db.Column(db.Integer, db.ForeignKey('user.id'), unique=True, nullable=False) - artist_photos = db.relationship("Photo", backref="artist") - artist_videos = db.relationship("Video", backref="artist") - artist_music = db.relationship("Music", backref="artist") + artist_photos = db.relationship("Photo", backref="artist_profile") + artist_videos = db.relationship("Video", backref="artist_profile") + artist_music = db.relationship("Music", backref="artist_profile") def __repr__(self): return f'<Artist_Profile {self.id}>' @@ -88,7 +88,7 @@ class Photo(db.Model): media_url = db.Column(db.Text, nullable=False) # Cloudinary URL # Relationships - artist_id = db.Column(db.Integer, db.ForeignKey("artist.id"), nullable=False) + artist_id = db.Column(db.Integer, db.ForeignKey("artist_profile.id"), nullable=False) def __repr__(self): return f'<Photo {self.title}>' @@ -111,7 +111,7 @@ class Video(db.Model): duration = db.Column(db.Integer, nullable=False) # Relationships - artist_id = db.Column(db.Integer, db.ForeignKey("artist.id"), nullable=False) + artist_id = db.Column(db.Integer, db.ForeignKey("artist_profile.id"), nullable=False) def __repr__(self): return f'<Video {self.title}>' @@ -149,7 +149,7 @@ class Music(db.Model): duration = db.Column(db.Integer, nullable=False) # Relationships - artist_id = db.Column(db.Integer, db.ForeignKey("artist.id"), nullable=False) + artist_id = db.Column(db.Integer, db.ForeignKey("artist_profile.id"), nullable=False) def __repr__(self): return f'<Music {self.title}>' @@ -189,7 +189,7 @@ class Follow_Artist(db.Model): __tablename__ = "follow_artist" user_id = db.Column(db.Integer, db.ForeignKey("user.id"), primary_key=True) - artist_id = db.Column(db.Integer, db.ForeignKey("artist.id"), primary_key=True) + artist_id = db.Column(db.Integer, db.ForeignKey("artist_profile.id"), primary_key=True) def __repr__(self): return f'<Follow_Artist user_id={self.user_id}, artist_id={self.artist_id}, is_active={self.is_active}>' From 9587dfee19c401b95e95da545e341c667e8e2115 Mon Sep 17 00:00:00 2001 From: Rafael Salinas Gonzalez <rafsalgon@gmail.com> Date: Fri, 7 Mar 2025 18:41:30 +0000 Subject: [PATCH 19/25] Created videos routes --- Pipfile.lock | 653 +--------------------------------------------- src/api/routes.py | 69 ++++- 2 files changed, 70 insertions(+), 652 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 35fcf34ad2..453d225727 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,7 @@ { "_meta": { "hash": { - - "sha256": "df7144212363e2fb107242f0448476de13fa283a57b574ff560e2a499fbc4049" - - "sha256": "565f98e9600186b666ea99818452df31f2af6834fd198eaf1fcc557fed5d866b" - + "sha256": "f330cd23b0270ece3f391c1a9745b0587e3adc87478d87f3d8a1e118c43e72c3" }, "pipfile-spec": 6, "requires": { @@ -19,651 +15,6 @@ } ] }, - "default": { - "alembic": { - "hashes": [ - "sha256:197de710da4b3e91cf66a826a5b31b5d59a127ab41bd0fc42863e2902ce2bbbe", - "sha256:e1a1c738577bca1f27e68728c910cd389b9a92152ff91d902da649c192e30c49" - ], - "markers": "python_version >= '3.9'", - "version": "==1.15.1" - }, - "blinker": { - "hashes": [ - "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", - "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc" - ], - "markers": "python_version >= '3.9'", - "version": "==1.9.0" - }, - "certifi": { - "hashes": [ - "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", - "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe" - ], - "markers": "python_version >= '3.6'", - "version": "==2025.1.31" - }, - "charset-normalizer": { - "hashes": [ - "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", - "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa", - "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a", - "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", - "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b", - "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", - "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", - "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", - "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", - "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", - "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", - "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", - "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", - "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", - "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", - "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", - "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", - "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", - "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", - "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", - "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e", - "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a", - "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4", - "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca", - "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", - "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", - "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", - "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", - "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", - "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", - "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", - "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", - "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", - "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", - "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", - "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd", - "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c", - "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", - "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", - "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", - "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", - "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824", - "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", - "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf", - "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487", - "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d", - "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd", - "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", - "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534", - "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", - "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", - "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", - "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd", - "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", - "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9", - "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", - "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", - "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d", - "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", - "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", - "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", - "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", - "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", - "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", - "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8", - "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", - "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", - "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", - "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", - "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", - "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", - "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", - "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", - "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", - "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", - "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", - "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", - "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e", - "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6", - "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", - "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", - "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e", - "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", - "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", - "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c", - "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", - "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", - "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089", - "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", - "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e", - "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", - "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616" - ], - "markers": "python_version >= '3.7'", - "version": "==3.4.1" - }, - "click": { - "hashes": [ - "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", - "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a" - ], - "markers": "python_version >= '3.7'", - "version": "==8.1.8" - }, - "cloudinary": { - "hashes": [ - "sha256:ba223705409b2aaddd5196c2184d65f50a83dffcba3b94f3727658ff6a0172a3", - "sha256:e4191b470c5bae55542b64e0a78659af42971880294456dca480bc974fa9280a" - ], - "index": "pypi", - "version": "==1.42.2" - }, - "flask": { - "hashes": [ - "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac", - "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136" - ], - "index": "pypi", - "version": "==3.1.0" - }, - "flask-admin": { - "hashes": [ - "sha256:24cae2af832b6a611a01d7dc35f42d266c1d6c75a426b869d8cb241b78233369", - "sha256:fd8190f1ec3355913a22739c46ed3623f1d82b8112cde324c60a6fc9b21c9406" - ], - "index": "pypi", - "version": "==1.6.1" - }, - "flask-cors": { - "hashes": [ - "sha256:6ccb38d16d6b72bbc156c1c3f192bc435bfcc3c2bc864b2df1eb9b2d97b2403c", - "sha256:fa5cb364ead54bbf401a26dbf03030c6b18fb2fcaf70408096a572b409586b0c" - - ], - "index": "pypi", - "version": "==5.0.1" - }, - "flask-extended": { - "hashes": [ - "sha256:7d4951573da1fb680185970bfb6427934809ddd4a7d372acf67736d4455ae7be", - "sha256:dcf785327209e1967b01e62868f3ff6bc1401a2359746972111969dc16a7fd29" - ], - "index": "pypi", - "version": "==0.2" - ], - "index": "pypi", - "version": "==5.0.1" - }, - "flask-jwt-extended": { - "hashes": [ - "sha256:52f35bf0985354d7fb7b876e2eb0e0b141aaff865a22ff6cc33d9a18aa987978", - "sha256:8085d6757505b6f3291a2638c84d207e8f0ad0de662d1f46aa2f77e658a0c976" - ], - "index": "pypi", - "version": "==4.7.1" - }, - "flask-migrate": { - "hashes": [ - "sha256:1a336b06eb2c3ace005f5f2ded8641d534c18798d64061f6ff11f79e1434126d", - "sha256:24d8051af161782e0743af1b04a152d007bad9772b2bca67b7ec1e8ceeb3910d" - ], - "index": "pypi", - "version": "==4.1.0" - }, - "flask-sqlalchemy": { - "hashes": [ - "sha256:c5765e58ca145401b52106c0f46178569243c5da25556be2c231ecc60867c5b1", - "sha256:cabb6600ddd819a9f859f36515bb1bd8e7dbf30206cc679d2b081dff9e383283" - ], - "index": "pypi", - "version": "==3.0.5" - }, - "flask-swagger": { - "hashes": [ - "sha256:3caddb1311388eafc86f82f8e64ba386a5df6b84e5f16dfae19ca08173eba216", - "sha256:b4085f5bc36df4c20b6548cd1413adc9cf35719b0f0695367cd542065145294d" - ], - "index": "pypi", - "version": "==0.2.14" - }, - "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' 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" - }, - "gunicorn": { - "hashes": [ - "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", - "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec" - ], - "index": "pypi", - "version": "==23.0.0" - }, - "idna": { - "hashes": [ - "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", - "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" - ], - "markers": "python_version >= '3.6'", - "version": "==3.10" - }, - "itsdangerous": { - "hashes": [ - "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", - "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173" - ], - "markers": "python_version >= '3.8'", - "version": "==2.2.0" - }, - "jinja2": { - "hashes": [ - "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", - "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb" - ], - "markers": "python_version >= '3.7'", - "version": "==3.1.5" - }, - "mako": { - "hashes": [ - "sha256:95920acccb578427a9aa38e37a186b1e43156c87260d7ba18ca63aa4c7cbd3a1", - "sha256:b5d65ff3462870feec922dbccf38f6efb44e5714d7b593a656be86663d8600ac" - ], - "markers": "python_version >= '3.8'", - "version": "==1.3.9" - }, - "markupsafe": { - "hashes": [ - "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", - "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", - "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", - "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", - "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", - "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", - "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", - "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", - "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", - "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", - "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", - "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", - "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", - "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", - "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", - "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", - "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", - "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", - "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", - "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", - "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", - "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", - "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", - "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", - "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", - "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", - "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", - "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", - "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", - "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", - "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", - "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", - "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", - "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", - "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", - "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", - "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", - "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", - "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", - "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", - "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", - "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", - "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", - "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", - "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", - "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", - "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", - "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", - "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", - "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", - "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", - "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", - "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", - "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", - "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", - "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", - "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", - "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", - "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", - "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", - "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50" - ], - "markers": "python_version >= '3.9'", - "version": "==3.0.2" - }, - "packaging": { - "hashes": [ - "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", - "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" - ], - "markers": "python_version >= '3.8'", - "version": "==24.2" - }, - "psycopg2-binary": { - "hashes": [ - "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff", - "sha256:056470c3dc57904bbf63d6f534988bafc4e970ffd50f6271fc4ee7daad9498a5", - "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f", - "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5", - "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0", - "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c", - "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c", - "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341", - "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f", - "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", - "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", - "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007", - "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", - "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92", - "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb", - "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5", - "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5", - "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8", - "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1", - "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68", - "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73", - "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1", - "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53", - "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d", - "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906", - "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0", - "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", - "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a", - "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b", - "sha256:5c370b1e4975df846b0277b4deba86419ca77dbc25047f535b0bb03d1a544d44", - "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648", - "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7", - "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f", - "sha256:73aa0e31fa4bb82578f3a6c74a73c273367727de397a7a0f07bd83cbea696baa", - "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697", - "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d", - "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b", - "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526", - "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4", - "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287", - "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e", - "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673", - "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0", - "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30", - "sha256:8aecc5e80c63f7459a1a2ab2c64df952051df196294d9f739933a9f6687e86b3", - "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e", - "sha256:8de718c0e1c4b982a54b41779667242bc630b2197948405b7bd8ce16bcecac92", - "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a", - "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c", - "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8", - "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", - "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47", - "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864", - "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc", - "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00", - "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb", - "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539", - "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b", - "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481", - "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5", - "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4", - "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64", - "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392", - "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4", - "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", - "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1", - "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", - "sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863" - ], - "index": "pypi", - "version": "==2.9.10" - }, - "pyjwt": { - "hashes": [ - "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850", - "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c" - ], - "index": "pypi", - "version": "==2.9.0" - }, - "python-dotenv": { - "hashes": [ - "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", - "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a" - ], - "index": "pypi", - "version": "==1.0.1" - }, - "pyyaml": { - "hashes": [ - "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", - "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", - "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", - "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", - "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", - "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", - "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", - "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", - "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", - "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", - "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", - "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", - "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", - "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", - "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", - "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", - "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", - "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", - "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", - "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", - "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", - "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", - "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", - "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", - "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", - "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", - "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", - "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", - "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", - "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", - "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", - "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", - "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", - "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", - "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", - "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", - "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", - "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", - "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", - "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", - "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", - "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", - "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", - "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", - "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", - "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", - "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", - "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", - "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", - "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", - "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", - "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", - "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" - ], - "markers": "python_version >= '3.8'", - "version": "==6.0.2" - }, - "requests": { - "hashes": [ - "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", - "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" - ], - "index": "pypi", - "version": "==2.32.3" - }, - "six": { - "hashes": [ - "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", - "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.17.0" - }, - "sqlalchemy": { - "hashes": [ - "sha256:07e48cbcdda6b8bc7a59d6728bd3f5f574ffe03f2c9fb384239f3789c2d95c2e", - "sha256:18cafdb27834fa03569d29f571df7115812a0e59fd6a3a03ccb0d33678ec8420", - "sha256:1b1e5e96e2789d89f023d080bee432e2fef64d95857969e70d3cadec80bd26f0", - "sha256:315676344e3558f1f80d02535f410e80ea4e8fddba31ec78fe390eff5fb8f466", - "sha256:31de1e2c45e67a5ec1ecca6ec26aefc299dd5151e355eb5199cd9516b57340be", - "sha256:3d94682732d1a0def5672471ba42a29ff5e21bb0aae0afa00bb10796fc1e28dd", - "sha256:3ec187acf85984263299a3f15c34a6c0671f83565d86d10f43ace49881a82718", - "sha256:4847f4b1d822754e35707db913396a29d874ee77b9c3c3ef3f04d5a9a6209618", - "sha256:4d112b0f3c1bc5ff70554a97344625ef621c1bfe02a73c5d97cac91f8cd7a41e", - "sha256:51e1ba2884c6a2b8e19109dc08c71c49530006c1084156ecadfaadf5f9b8b053", - "sha256:535377e9b10aff5a045e3d9ada8a62d02058b422c0504ebdcf07930599890eb0", - "sha256:5dbf17ac9a61e7a3f1c7ca47237aac93cabd7f08ad92ac5b96d6f8dea4287fc1", - "sha256:5f752676fc126edc1c4af0ec2e4d2adca48ddfae5de46bb40adbd3f903eb2120", - "sha256:64cb0ad8a190bc22d2112001cfecdec45baffdf41871de777239da6a28ed74b6", - "sha256:6913b8247d8a292ef8315162a51931e2b40ce91681f1b6f18f697045200c4a30", - "sha256:69fac0a7054d86b997af12dc23f581cf0b25fb1c7d1fed43257dee3af32d3d6d", - "sha256:7001f16a9a8e06488c3c7154827c48455d1c1507d7228d43e781afbc8ceccf6d", - "sha256:7b81b1030c42b003fc10ddd17825571603117f848814a344d305262d370e7c34", - "sha256:7f8267682eb41a0584cf66d8a697fef64b53281d01c93a503e1344197f2e01fe", - "sha256:887865924c3d6e9a473dc82b70977395301533b3030d0f020c38fd9eba5419f2", - "sha256:9167d4227b56591a4cc5524f1b79ccd7ea994f36e4c648ab42ca995d28ebbb96", - "sha256:939f9a018d2ad04036746e15d119c0428b1e557470361aa798e6e7d7f5875be0", - "sha256:955162ad1a931fe416eded6bb144ba891ccbf9b2e49dc7ded39274dd9c5affc5", - "sha256:984ee13543a346324319a1fb72b698e521506f6f22dc37d7752a329e9cd00a32", - "sha256:9883f5fae4fd8e3f875adc2add69f8b945625811689a6c65866a35ee9c0aea23", - "sha256:a1ad90c97029cc3ab4ffd57443a20fac21d2ec3c89532b084b073b3feb5abff3", - "sha256:a3714e5b33226131ac0da60d18995a102a17dddd42368b7bdd206737297823ad", - "sha256:ae067ab639fa499f67ded52f5bc8e084f045d10b5ac7bb928ae4ca2b6c0429a5", - "sha256:b33ffbdbbf5446cf36cd4cc530c9d9905d3c2fe56ed09e25c22c850cdb9fac92", - "sha256:b6e4cb5c63f705c9d546a054c60d326cbde7421421e2d2565ce3e2eee4e1a01f", - "sha256:b7f4b6aa6e87991ec7ce0e769689a977776db6704947e562102431474799a857", - "sha256:c04144a24103135ea0315d459431ac196fe96f55d3213bfd6d39d0247775c854", - "sha256:c522e496f9b9b70296a7675272ec21937ccfc15da664b74b9f58d98a641ce1b6", - "sha256:c5a99282848b6cae0056b85da17392a26b2d39178394fc25700bcf967e06e97a", - "sha256:c7a46639ba058d320c9f53a81db38119a74b8a7a1884df44d09fbe807d028aaf", - "sha256:d4b1cc7835b39835c75cf7c20c926b42e97d074147c902a9ebb7cf2c840dc4e2", - "sha256:d4d164df3d83d204c69f840da30b292ac7dc54285096c6171245b8d7807185aa", - "sha256:d61e9ecc849d8d44d7f80894ecff4abe347136e9d926560b818f6243409f3c86", - "sha256:d68e1762997bfebf9e5cf2a9fd0bcf9ca2fdd8136ce7b24bbd3bbfa4328f3e4a", - "sha256:e3c1808008124850115a3f7e793a975cfa5c8a26ceeeb9ff9cbb4485cac556df", - "sha256:f8cb80fe8d14307e4124f6fad64dfd87ab749c9d275f82b8b4ec84c84ecebdbe" - ], - "index": "pypi", - "version": "==1.4.46" - }, - "typing-extensions": { - "hashes": [ - "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", - "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" - ], - "index": "pypi", - "version": "==4.12.2" - }, - "urllib3": { - "hashes": [ - "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", - "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d" - ], - "markers": "python_version >= '3.9'", - "version": "==2.3.0" - }, - "werkzeug": { - "hashes": [ - "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", - "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746" - ], - "markers": "python_version >= '3.9'", - "version": "==3.1.3" - }, - "wtforms": { - "hashes": [ - "sha256:bf831c042829c8cdbad74c27575098d541d039b1faa74c771545ecac916f2c07", - "sha256:f8d76180d7239c94c6322f7990ae1216dae3659b7aa1cee94b6318bdffb474b9" - ], - "index": "pypi", - "version": "==3.1.2" - } - }, + "default": {}, "develop": {} } diff --git a/src/api/routes.py b/src/api/routes.py index 96046518c9..bfceae764f 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -2,7 +2,7 @@ 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, Genre +from api.models import db, User, Genre, ArtistProfile, Photo, Video, Music, SavedMusic, FollowArtist from api.utils import generate_sitemap, APIException from flask_cors import CORS import os @@ -115,3 +115,70 @@ def getGenres(): genres = Genre.query.filter(Genre.id != 0).all() return jsonify({"genres": [genre.serialize() for genre in genres]}), 200 + + + + + + + + + + + + + # GET: Obtener videos +@api.route('/artist/<int:artist_id>/videos', methods=['GET']) +def get_artist_videos(artist_id): + # Verifica si el artista existe (descomenta o ajusta según tu modelo real) + artist = ArtistProfile.query.get(artist_id) + if not artist: + return jsonify({"msg": "Artista no encontrado"}), 404 + + # Obtén los videos para este artista + videos = Video.query.filter_by(artist_id=artist_id).all() + serialized_videos = [v.serialize() for v in videos] + return jsonify(serialized_videos), 200 + + # POST: Guardar nuevo video +@api.route('/artist/<int:artist_id>/videos', methods=['POST']) +def create_artist_video(artist_id): + artist = ArtistProfile.query.get(artist_id) + if not artist: + return jsonify({"msg": "Artista no encontrado"}), 404 + + body = request.get_json() + if not body: + return jsonify({"msg": "No data provided"}), 400 + + media_url = body.get("media_url") + title = body.get("title", "Sin título") + + if not media_url: + return jsonify({"msg": "media_url is required"}), 400 + + # Creamos el video + new_video = Video( + title=title, + media_url=media_url, + artist_id=artist_id + ) + db.session.add(new_video) + db.session.commit() + + return jsonify(new_video.serialize()), 201 + + # DELETE: Eliminar video +@api.route('/artist/<int:artist_id>/videos/<int:video_id>', methods=['DELETE']) +def delete_artist_video(artist_id, video_id): + artist = ArtistProfile.query.get(artist_id) + if not artist: + return jsonify({"msg": "Artista no encontrado"}), 404 + + video = Video.query.filter_by(id=video_id, artist_id=artist_id).first() + if not video: + return jsonify({"msg": "Video no encontrado"}), 404 + + db.session.delete(video) + db.session.commit() + return jsonify({"msg": "Video eliminado con éxito"}), 200 From a6f5a873a6dae8ef74310e5fd3958bf7c346008f Mon Sep 17 00:00:00 2001 From: Rafael Salinas Gonzalez <rafsalgon@gmail.com> Date: Fri, 7 Mar 2025 20:06:28 +0000 Subject: [PATCH 20/25] Music routes created --- src/api/routes.py | 68 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/src/api/routes.py b/src/api/routes.py index bfceae764f..19a3d3bcd0 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -96,6 +96,7 @@ def generate_token(): def get_current_user(): return jsonify(current_user.serialize()), 200 + # ROUTE TO LOAD GENRES @api.route('/getGenresapi', methods=["GET"]) def getGenresApi(): @@ -119,14 +120,6 @@ def getGenres(): - - - - - - - - # GET: Obtener videos @api.route('/artist/<int:artist_id>/videos', methods=['GET']) def get_artist_videos(artist_id): @@ -182,3 +175,62 @@ def delete_artist_video(artist_id, video_id): db.session.delete(video) db.session.commit() return jsonify({"msg": "Video eliminado con éxito"}), 200 + + + + + # GET: Obtener musica +@api.route('/artist/<int:artist_id>/songs', methods=['GET']) +def get_artist_songs(artist_id): + # Verifica si el artista existe (descomenta o ajusta según tu modelo real) + artist = ArtistProfile.query.get(artist_id) + if not artist: + return jsonify({"msg": "Artista no encontrado"}), 404 + + # Obtén la musica para este artista + songs = Music.query.filter_by(artist_id=artist_id).all() + serialized_songs = [s.serialize() for s in songs] + return jsonify(serialized_songs), 200 + + # POST: Guardar nueva musica +@api.route('/artist/<int:artist_id>/songs', methods=['POST']) +def create_artist_song(artist_id): + artist = ArtistProfile.query.get(artist_id) + if not artist: + return jsonify({"msg": "Artista no encontrado"}), 404 + + body = request.get_json() + if not body: + return jsonify({"msg": "No data provided"}), 400 + + media_url = body.get("media_url") + title = body.get("title", "Sin título") + + if not media_url: + return jsonify({"msg": "media_url is required"}), 400 + + # Creamos la musica + new_song = Music( + title=title, + media_url=media_url, + artist_id=artist_id + ) + db.session.add(new_song) + db.session.commit() + + return jsonify(new_song.serialize()), 201 + + # DELETE: Eliminar musica +@api.route('/artist/<int:artist_id>/songs/<int:song_id>', methods=['DELETE']) +def delete_artist_song(artist_id, song_id): + artist = ArtistProfile.query.get(artist_id) + if not artist: + return jsonify({"msg": "Artista no encontrado"}), 404 + + song = Music.query.filter_by(id=song_id, artist_id=artist_id).first() + if not song: + return jsonify({"msg": "Cancion no encontrada"}), 404 + + db.session.delete(song) + db.session.commit() + return jsonify({"msg": "Cancion eliminada con éxito"}), 200 \ No newline at end of file From 834e1f99ee62297dd5a2f08e84eb859699d3e051 Mon Sep 17 00:00:00 2001 From: Rafael Salinas Gonzalez <rafsalgon@gmail.com> Date: Sat, 8 Mar 2025 15:03:20 +0000 Subject: [PATCH 21/25] Footer done --- .vscode/settings.json | 3 +++ src/api/models.py | 10 +++++----- src/front/js/component/footer.js | 16 ++++++++-------- src/front/js/pages/home.js | 13 ------------- src/front/styles/footer.css | 12 ++++++++++++ 5 files changed, 28 insertions(+), 26 deletions(-) create mode 100644 src/front/styles/footer.css diff --git a/.vscode/settings.json b/.vscode/settings.json index 7bd07e2e82..6e34222a2e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,5 +6,8 @@ }, "[javascript]": { "editor.defaultFormatter": "vscode.typescript-language-features" + }, + "[css]": { + "editor.defaultFormatter": "vscode.css-language-features" } } diff --git a/src/api/models.py b/src/api/models.py index 716924a36c..dd7b1d8e06 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -22,8 +22,8 @@ class User(db.Model): profile_photo = db.Column(db.String(255), nullable=True) # Profile photo URL # Relationships - followed_artists = db.relationship('Follow_Artist', backref='follow_artist') - saved_music = db.relationship('Saved_Music', backref='user') + followed_artists = db.relationship('FollowArtist', backref='follow_artist') + saved_music = db.relationship('SavedMusic', backref='user') def __repr__(self): return f'<User {self.username}>' @@ -53,7 +53,7 @@ def check_password(self,password): # ARTIST PROFILE MODEL -class Artist_Profile(db.Model): +class ArtistProfile(db.Model): __tablename__ = "artist_profile" id = db.Column(db.Integer, primary_key=True) @@ -164,7 +164,7 @@ def serialize(self): # USER SAVED MUSIC & FOLLOW ARTIST MODEL -class Saved_Music(db.Model): +class SavedMusic(db.Model): __tablename__ = "saved_music" id = db.Column(db.Integer, primary_key=True) @@ -185,7 +185,7 @@ def serialize(self): # FOLLOW ARTIST MODEL -class Follow_Artist(db.Model): +class FollowArtist(db.Model): __tablename__ = "follow_artist" user_id = db.Column(db.Integer, db.ForeignKey("user.id"), primary_key=True) diff --git a/src/front/js/component/footer.js b/src/front/js/component/footer.js index 670323e091..a0a8032668 100755 --- a/src/front/js/component/footer.js +++ b/src/front/js/component/footer.js @@ -1,10 +1,10 @@ import React, { Component } from "react"; +import "../../styles/footer.css"; -export const Footer = () => ( - <footer className="footer mt-auto py-3 text-center"> - <p> - Made with <i className="fa fa-heart text-danger" /> by{" "} - <a href="http://www.4geeksacademy.com">4Geeks Academy</a> - </p> - </footer> -); +export const Footer = () => { + return ( + <footer className="footer"> + <p className="text">© 2025 SoundCript. All Rights Reserved.</p> + </footer> + ) +}; diff --git a/src/front/js/pages/home.js b/src/front/js/pages/home.js index 7b0be46a1e..458bd892f1 100755 --- a/src/front/js/pages/home.js +++ b/src/front/js/pages/home.js @@ -8,19 +8,6 @@ export const Home = () => { return ( <div className="text-center mt-5"> - <h1>Hello Rigo!!</h1> - <p> - <img src={rigoImageUrl} /> - </p> - <div className="alert alert-info"> - {store.message || "Loading message from the backend (make sure your python backend is running)..."} - </div> - <p> - This boilerplate comes with lots of documentation:{" "} - <a href="https://start.4geeksacademy.com/starters/react-flask"> - Read documentation - </a> - </p> </div> ); }; diff --git a/src/front/styles/footer.css b/src/front/styles/footer.css new file mode 100644 index 0000000000..86da241e79 --- /dev/null +++ b/src/front/styles/footer.css @@ -0,0 +1,12 @@ +.footer { + width: 100%; + margin-top: 3%; + border-radius: 10px; + display: grid; + place-items: center; + height: 6%; + background: linear-gradient(90deg, + var(--color-red) 0%, + var(--color-giants-orange) 50%, + var(--color-atomic-tangerine) 100%); +} From da49249ae4656eaafbed3ed54df5843a2d23a690 Mon Sep 17 00:00:00 2001 From: Rafael Salinas Gonzalez <rafsalgon@gmail.com> Date: Mon, 10 Mar 2025 14:14:46 +0000 Subject: [PATCH 22/25] SavedSong, FollowArtist and GET user favourite routes created + Music model modified to Song --- src/api/models.py | 28 +++++++------- src/api/routes.py | 94 ++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 102 insertions(+), 20 deletions(-) diff --git a/src/api/models.py b/src/api/models.py index dd7b1d8e06..0cba1aee0a 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -22,8 +22,8 @@ class User(db.Model): profile_photo = db.Column(db.String(255), nullable=True) # Profile photo URL # Relationships - followed_artists = db.relationship('FollowArtist', backref='follow_artist') - saved_music = db.relationship('SavedMusic', backref='user') + followed_artist = db.relationship('FollowArtist', backref='follow_artist') + saved_song = db.relationship('SavedSong', backref='user') def __repr__(self): return f'<User {self.username}>' @@ -64,10 +64,10 @@ class ArtistProfile(db.Model): artist_photos = db.relationship("Photo", backref="artist_profile") artist_videos = db.relationship("Video", backref="artist_profile") - artist_music = db.relationship("Music", backref="artist_profile") + artist_songs = db.relationship("Song", backref="artist_profile") def __repr__(self): - return f'<Artist_Profile {self.id}>' + return f'<ArtistProfile {self.id}>' def serialize(self): return { @@ -76,7 +76,7 @@ def serialize(self): "bio": self.bio, "artist_photos": self.artist_photos, "artist_videos": self.artist_videos, - "artist_music": self.artist_music + "artist_songs": self.artist_songs } # ARTIST PHOTO MODEL @@ -140,8 +140,8 @@ def serialize(self): } # ARTIST MUSIC MODEL -class Music(db.Model): - __tablename__ = "music" +class Song(db.Model): + __tablename__ = "song" id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(200), nullable=False) @@ -152,7 +152,7 @@ class Music(db.Model): artist_id = db.Column(db.Integer, db.ForeignKey("artist_profile.id"), nullable=False) def __repr__(self): - return f'<Music {self.title}>' + return f'<Song {self.title}>' def serialize(self): return { @@ -164,23 +164,23 @@ def serialize(self): # USER SAVED MUSIC & FOLLOW ARTIST MODEL -class SavedMusic(db.Model): - __tablename__ = "saved_music" +class SavedSong(db.Model): + __tablename__ = "saved_song" id = db.Column(db.Integer, primary_key=True) # Relationships user_id = db.Column(db.Integer, db.ForeignKey("user.id")) - music_id = db.Column(db.Integer, db.ForeignKey("music.id")) + song_id = db.Column(db.Integer, db.ForeignKey("song.id")) def __repr__(self): - return f'<Saved_Music {self.music_id}>' + return f'<Saved_Song {self.song_id}>' def serialize(self): return { "id": self.id, "user_id": self.user_id, - "music_id": self.music_id + "song_id": self.song_id } @@ -192,7 +192,7 @@ class FollowArtist(db.Model): artist_id = db.Column(db.Integer, db.ForeignKey("artist_profile.id"), primary_key=True) def __repr__(self): - return f'<Follow_Artist user_id={self.user_id}, artist_id={self.artist_id}, is_active={self.is_active}>' + return f'<FollowArtist user_id={self.user_id}, artist_id={self.artist_id}, is_active={self.is_active}>' def serialize(self): return { diff --git a/src/api/routes.py b/src/api/routes.py index 19a3d3bcd0..d0e38d0940 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -2,12 +2,12 @@ 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, Genre, ArtistProfile, Photo, Video, Music, SavedMusic, FollowArtist +from api.models import db, User, Genre, ArtistProfile, Photo, Video, Song, SavedSong, FollowArtist from api.utils import generate_sitemap, APIException from flask_cors import CORS import os -from flask_jwt_extended import create_access_token, current_user, jwt_required +from flask_jwt_extended import create_access_token, current_user, jwt_required, get_jwt_identity import requests @@ -188,7 +188,7 @@ def get_artist_songs(artist_id): return jsonify({"msg": "Artista no encontrado"}), 404 # Obtén la musica para este artista - songs = Music.query.filter_by(artist_id=artist_id).all() + songs = Song.query.filter_by(artist_id=artist_id).all() serialized_songs = [s.serialize() for s in songs] return jsonify(serialized_songs), 200 @@ -210,7 +210,7 @@ def create_artist_song(artist_id): return jsonify({"msg": "media_url is required"}), 400 # Creamos la musica - new_song = Music( + new_song = Song( title=title, media_url=media_url, artist_id=artist_id @@ -227,10 +227,92 @@ def delete_artist_song(artist_id, song_id): if not artist: return jsonify({"msg": "Artista no encontrado"}), 404 - song = Music.query.filter_by(id=song_id, artist_id=artist_id).first() + song = Song.query.filter_by(id=song_id, artist_id=artist_id).first() if not song: return jsonify({"msg": "Cancion no encontrada"}), 404 db.session.delete(song) db.session.commit() - return jsonify({"msg": "Cancion eliminada con éxito"}), 200 \ No newline at end of file + return jsonify({"msg": "Cancion eliminada con éxito"}), 200 + + + +# GET USER FAVOURITE SONGS AND ARTISTS +@api.route('/user/profile/<int:id>', methods=['GET']) +def handle_user_favourites(id): + # Find the user by id + user = User.query.get(id) + + if not user: + return jsonify({"ERROR": "Usuario no encontrado"}), 404 + + return jsonify({ + "saved_songs": [fav.serialize() for fav in user.saved_songs] if user.saved_songs else "No hay canciones guardadas", + "followed_artists": [fav.serialize() for fav in user.followed_artists] if user.followed_artists else "No tienes artistas guardados" + }), 200 + + + +# POST & DELETE FOR USER FAVOURITE SONGS +@api.route('/user/profile/<int:id>/favorite/songs/<int:song_id>', methods=['POST', 'DELETE']) +def handle_favourite_songs(song_id, id): + # Find the user by id + user = User.query.get(id) + + if not user: + return jsonify({"ERROR": "Usuario no encontrado"}), 404 + + # Check if the song is already liked/favourited by the user + existing_favourite_song = SavedSong.query.filter_by(user_id=id, song_id=song_id).first() + + # POST + if request.method == 'POST': + if existing_favourite_song: + return jsonify({"msg": "La canción ya está en favoritos"}), 400 + + new_favourite_song = SavedSong(user_id=id, song_id=song_id) + db.session.add(new_favourite_song) + db.session.commit() + return jsonify({"msg": "Canción añadida a favoritos con éxito", "new_favourite_song": new_favourite_song.serialize()}), 200 + + # DELETE + if request.method == 'DELETE': + if not existing_favourite_song: + return jsonify({"msg": "La canción no está en favoritos"}), 400 + + db.session.delete(existing_favourite_song) + db.session.commit() + return jsonify({"msg": "Canción eliminada de favoritos con éxito"}), 200 + + + +# POST & DELETE FOR USER FOLLOWED ARTISTS +@api.route('/user/profile/<int:id>/favorite/aritsts/<int:artist_id>', methods=['POST', 'DELETE']) +def handle_followed_artists(artist_id, id): + # Find the user by id + user = User.query.get(id) + + if not user: + return jsonify({"ERROR": "Usuario no encontrado"}), 404 + + # Check if the artist is already followed by the user + existing_followed_artist = FollowArtist.query.filter_by(user_id=id, artist_id=artist_id).first() + + # POST + if request.method == 'POST': + if existing_followed_artist: + return jsonify({"msg": "Ya sigues a este artista"}), 400 + + new_followed_artist = FollowArtist(user_id=id, artist_id=artist_id) + db.session.add(new_followed_artist) + db.session.commit() + return jsonify({"msg": "Artista seguido con éxito", "new_followed_artist": new_followed_artist.serialize()}), 200 + + # DELETE + if request.method == 'DELETE': + if not existing_followed_artist: + return jsonify({"msg": "No sigues a este artista"}), 400 + + db.session.delete(existing_followed_artist) + db.session.commit() + return jsonify({"msg": "Artista dejado de seguir con éxito"}), 200 From d10e9c7818b5e301e47184ad1f6172e7e04edd76 Mon Sep 17 00:00:00 2001 From: Oscar Parra <oscarparrapalomino@gmail.com> Date: Mon, 10 Mar 2025 15:32:53 +0000 Subject: [PATCH 23/25] comentario commit --- Pipfile | 3 +- Pipfile.lock | 16 +- src/api/admin.py | 8 +- src/api/models.py | 106 +++-- src/api/routes.py | 26 +- src/app.py | 4 +- src/front/js/layout.js | 12 +- src/front/js/pages/{genre.js => HomeUser.js} | 2 +- src/front/js/pages/{home.js => genre2.js} | 2 +- src/front/js/pages/login.js | 433 ++++++++++--------- 10 files changed, 327 insertions(+), 285 deletions(-) rename src/front/js/pages/{genre.js => HomeUser.js} (98%) rename src/front/js/pages/{home.js => genre2.js} (95%) diff --git a/Pipfile b/Pipfile index 6429bd1f18..9a459ca534 100644 --- a/Pipfile +++ b/Pipfile @@ -20,10 +20,9 @@ flask-admin = "*" typing-extensions = "*" wtforms = "==3.1.2" flask-extended = "*" +requests = "*" flask-jwt-extended = "*" pyjwt = "==2.9.0" -requests = "*" - [requires] python_version = "3.10" diff --git a/Pipfile.lock b/Pipfile.lock index 35fcf34ad2..86740c5d72 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,7 @@ { "_meta": { "hash": { - - "sha256": "df7144212363e2fb107242f0448476de13fa283a57b574ff560e2a499fbc4049" - - "sha256": "565f98e9600186b666ea99818452df31f2af6834fd198eaf1fcc557fed5d866b" - + "sha256": "f330cd23b0270ece3f391c1a9745b0587e3adc87478d87f3d8a1e118c43e72c3" }, "pipfile-spec": 6, "requires": { @@ -178,7 +174,6 @@ "hashes": [ "sha256:6ccb38d16d6b72bbc156c1c3f192bc435bfcc3c2bc864b2df1eb9b2d97b2403c", "sha256:fa5cb364ead54bbf401a26dbf03030c6b18fb2fcaf70408096a572b409586b0c" - ], "index": "pypi", "version": "==5.0.1" @@ -190,9 +185,6 @@ ], "index": "pypi", "version": "==0.2" - ], - "index": "pypi", - "version": "==5.0.1" }, "flask-jwt-extended": { "hashes": [ @@ -331,11 +323,11 @@ }, "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": [ diff --git a/src/api/admin.py b/src/api/admin.py index 1979841fc9..58410e318a 100644 --- a/src/api/admin.py +++ b/src/api/admin.py @@ -1,7 +1,7 @@ import os from flask_admin import Admin -from .models import db, User, Genre +from .models import db, User, Genre, ArtistProfile,Photo,Video,Music,SavedMusic,FollowArtist from flask_admin.contrib.sqla import ModelView def setup_admin(app): @@ -13,6 +13,12 @@ def setup_admin(app): # Add your models here, for example this is how we add a the User model to the admin admin.add_view(ModelView(User, db.session)) admin.add_view(ModelView(Genre, db.session)) + admin.add_view(ModelView(Photo, db.session)) + admin.add_view(ModelView(Video, db.session)) + admin.add_view(ModelView(Music, db.session)) + admin.add_view(ModelView(SavedMusic, db.session)) + admin.add_view(ModelView(FollowArtist, db.session)) + admin.add_view(ModelView(ArtistProfile, db.session)) # You can duplicate that line to add mew models # admin.add_view(ModelView(YourModelName, db.session)) \ No newline at end of file diff --git a/src/api/models.py b/src/api/models.py index 9e53b9a66b..9522a847b4 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -1,35 +1,32 @@ from flask_sqlalchemy import SQLAlchemy -from werkzeug.security import generate_password_hash, check_password_hash +from werkzeug.security import generate_password_hash, check_password_hash db = SQLAlchemy() - # USER REGISTER AND PROFILE MODEL +# USER REGISTER AND PROFILE MODEL class User(db.Model): __tablename__ = "user" id = db.Column(db.Integer, primary_key=True) - fullName = db.Column(db.String(120), unique=True, nullable=True) username = db.Column(db.String(120), unique=True, nullable=True) address = db.Column(db.String(120), unique=True, nullable=False) - email = db.Column(db.String(120), unique=True, nullable=False) password_hash = db.Column(db.String(512), unique=False, nullable=False) - - is_artist = db.Column(db.Boolean, default=False) # Check if account is artist + is_artist = db.Column(db.Boolean, default=False) # Check if account is artist is_active = db.Column(db.Boolean(), default=True) profile_photo = db.Column(db.String(255), nullable=True) # Profile photo URL - # Relationships - followed_artists = db.relationship('Follow_Artist', backref='follow_artist') - saved_music = db.relationship('Saved_Music', backref='user') + # Relationships + followed_artists = db.relationship('FollowArtist', backref='follow_artist') + saved_music = db.relationship('SavedMusic', backref='user') def __repr__(self): return f'<User {self.username}>' - def artist(is_artist): - if is_artist: + def artist(self): + if self.is_artist: return "Si" else: return "No" @@ -45,41 +42,41 @@ def serialize(self): "profile_photo": self.profile_photo, } -def set_password(self, password): - self.password_hash = generate_password_hash(password) + def set_password(self, password): + self.password_hash = generate_password_hash(password) -def check_password(self,password): - return check_password_hash(self.password_hash,password) + def check_password(self,password): + return check_password_hash(self.password_hash,password) - # ARTIST PROFILE MODEL -class Artist_Profile(db.Model): +# ARTIST PROFILE MODEL +class ArtistProfile(db.Model): __tablename__ = "artist_profile" id = db.Column(db.Integer, primary_key=True) bio = db.Column(db.Text, nullable=True) # Relationships - artist_id = db.Column(db.Integer, db.ForeignKey('users.id'), unique=True, nullable=False) + artist_id = db.Column(db.Integer, db.ForeignKey('user.id'), unique=True, nullable=False) - artist_photos = db.relationship("Photo", backref="artist") - artist_videos = db.relationship("Video", backref="artist") - artist_music = db.relationship("Music", backref="artist") + artist_photos = db.relationship("Photo", backref="artist_profile") + artist_videos = db.relationship("Video", backref="artist_profile") + artist_music = db.relationship("Music", backref="artist_profile") def __repr__(self): - return f'<Artist_Profile {self.id}>' + return f'<ArtistProfile {self.id}>' def serialize(self): return { "id": self.id, "artist_id": self.artist_id, "bio": self.bio, - "artist_photos": self.artist_photos, - "artist_videos": self.artist_videos, - "artist_music": self.artist_music + "artist_photos": [photo.serialize() for photo in self.artist_photos], + "artist_videos": [video.serialize() for video in self.artist_videos], + "artist_music": [music.serialize() for music in self.artist_music] } - - # ARTIST PHOTO MODEL + +# ARTIST PHOTO MODEL class Photo(db.Model): __tablename__ = "photo" @@ -88,7 +85,7 @@ class Photo(db.Model): media_url = db.Column(db.Text, nullable=False) # Cloudinary URL # Relationships - artist_id = db.Column(db.Integer, db.ForeignKey("artist.id"), nullable=False) + artist_profile_id = db.Column(db.Integer, db.ForeignKey("artist_profile.id"), nullable=False) def __repr__(self): return f'<Photo {self.title}>' @@ -101,7 +98,7 @@ def serialize(self): } - # ARTIST VIDEO MODEL +# ARTIST VIDEO MODEL class Video(db.Model): __tablename__ = "video" @@ -111,7 +108,7 @@ class Video(db.Model): duration = db.Column(db.Integer, nullable=False) # Relationships - artist_id = db.Column(db.Integer, db.ForeignKey("artist.id"), nullable=False) + artist_profile_id = db.Column(db.Integer, db.ForeignKey("artist_profile.id"), nullable=False) def __repr__(self): return f'<Video {self.title}>' @@ -124,22 +121,8 @@ def serialize(self): "duration": self.duration, } - # GENRES MODEL TO LOAD GENRES -class Genre(db.Model): - - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(250), unique=True, nullable=False) - def __repr__(self): - return f'<Genre {self.name}>' - - def serialize(self): - return { - "id": self.id, - "name": self.name - } - - # ARTIST MUSIC MODEL +# ARTIST MUSIC MODEL class Music(db.Model): __tablename__ = "music" @@ -149,7 +132,7 @@ class Music(db.Model): duration = db.Column(db.Integer, nullable=False) # Relationships - artist_id = db.Column(db.Integer, db.ForeignKey("artist.id"), nullable=False) + artist_profile_id = db.Column(db.Integer, db.ForeignKey("artist_profile.id"), nullable=False) def __repr__(self): return f'<Music {self.title}>' @@ -163,8 +146,8 @@ def serialize(self): } - # USER SAVED MUSIC & FOLLOW ARTIST MODEL -class Saved_Music(db.Model): +# USER SAVED MUSIC & FOLLOW ARTIST MODEL +class SavedMusic(db.Model): __tablename__ = "saved_music" id = db.Column(db.Integer, primary_key=True) @@ -174,7 +157,7 @@ class Saved_Music(db.Model): music_id = db.Column(db.Integer, db.ForeignKey("music.id")) def __repr__(self): - return f'<Saved_Music {self.music_id}>' + return f'<SavedMusic {self.music_id}>' def serialize(self): return { @@ -182,20 +165,33 @@ def serialize(self): "user_id": self.user_id, "music_id": self.music_id } - - # FOLLOW ARTIST MODEL -class Follow_Artist(db.Model): + +# FOLLOW ARTIST MODEL +class FollowArtist(db.Model): __tablename__ = "follow_artist" user_id = db.Column(db.Integer, db.ForeignKey("user.id"), primary_key=True) - artist_id = db.Column(db.Integer, db.ForeignKey("artist.id"), primary_key=True) + artist_profile_id = db.Column(db.Integer, db.ForeignKey("artist_profile.id"), primary_key=True) def __repr__(self): - return f'<Follow_Artist user_id={self.user_id}, artist_id={self.artist_id}, is_active={self.is_active}>' + return f'<Follow_Artist user_id={self.user_id}, artist_profile_id={self.artist_profile_id}>' def serialize(self): return { "user_id": self.user_id, - "artist_id": self.artist_id, + "artist_profile_id": self.artist_profile_id, } +class Genre(db.Model): + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(250), unique=True, nullable=False) + + def __repr__(self): + return f'<Genre {self.name}>' + + def serialize(self): + return { + "id": self.id, + "name": self.name + } \ No newline at end of file diff --git a/src/api/routes.py b/src/api/routes.py index 96046518c9..aad7dea1e4 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -49,6 +49,8 @@ def upload_image(): + + # Creacion de usuario @api.route('/register', methods=['POST']) def register(): @@ -57,6 +59,7 @@ def register(): address = request.json.get('address', None) email = request.json.get('email', None) password = request.json.get('password', None) + is_artist = request.json.get(True,None) if not fullName or not username or not email or not password: return jsonify({"msg": "Missing required fields"}), 400 @@ -69,7 +72,7 @@ def register(): if existing_user: return jsonify({"msg": "Username already exists"}), 400 - user = User(fullName=fullName, username=username, address=address, email=email, is_artist=True, is_active=True) + user = User(fullName=fullName, username=username, address=address, email=email, is_artist=is_artist, is_active=True) user.set_password(password) db.session.add(user) @@ -82,14 +85,27 @@ def generate_token(): username = request.json.get("username", None) password = request.json.get("password", None) + # Buscar al usuario por el nombre de usuario user = User.query.filter_by(username=username).one_or_none() + # Verificar si el usuario existe y la contraseña es correcta if not user or not user.check_password(password): - return jsonify("Wrong username or password"), 401 + return jsonify({"message": "Wrong username or password"}), 401 - access_token = create_access_token(identity=user) # Se pasa user.id - return jsonify(access_token=access_token) - + # Crear el token de acceso + access_token = create_access_token(identity=user) + + # Redirigir dependiendo de si es artista o no + if user.is_artist: + return jsonify({ + "access_token": access_token, + "redirect_url": f"/user/{user.id}" + }) + else: + return jsonify({ + "access_token": access_token, + "redirect_url": "/homeuser" + }) @api.route('/profile', methods=['GET']) @jwt_required() diff --git a/src/app.py b/src/app.py index 2ad028d6a2..2459f08a9e 100644 --- a/src/app.py +++ b/src/app.py @@ -20,7 +20,7 @@ app = Flask(__name__) 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( @@ -32,6 +32,8 @@ MIGRATE = Migrate(app, db, compare_type=True) db.init_app(app) + + # add the admin setup_admin(app) diff --git a/src/front/js/layout.js b/src/front/js/layout.js index f4abf9b78f..1b7dbc09d9 100755 --- a/src/front/js/layout.js +++ b/src/front/js/layout.js @@ -3,7 +3,7 @@ import { BrowserRouter, Route, Routes, useLocation } from "react-router-dom"; import ScrollToTop from "./component/scrollToTop"; import { BackendURL } from "./component/backendURL"; -import { Home } from "./pages/home"; +import { Genre2 } from "./pages/genre2"; import { Demo } from "./pages/demo"; import { Single } from "./pages/single"; import injectContext from "./store/appContext"; @@ -13,7 +13,7 @@ import { UserProfile } from "./pages/UserProfile"; // ✅ Nueva vista import { SavedSongs } from "./pages/SavedSongs"; // ✅ Nueva vista import { SavedArtists } from "./pages/SavedArtists"; // ✅ Nueva vista import ArtistProfile from "./pages/ArtistProfile"; -import { Genre } from "./pages/genre"; +import { HomeUser } from "./pages/HomeUser"; import { UserData } from "./pages/UserData" import { ArtistData } from "./pages/ArtistData" import { Navbar } from "./component/navbar"; @@ -39,18 +39,18 @@ const Layout = () => { const LayoutContent = () => { const location = useLocation(); // Obtiene la ruta actual - const hideNavbarFooter = location.pathname === "/login"; // Solo oculta en /login + const hideNavbarFooter = location.pathname === "/"; // Solo oculta en /login return ( <> {!hideNavbarFooter && <Navbar />} <Routes> - <Route element={<Home />} path="/" /> - <Route element={<Login />} path="/login" /> + <Route element={<Genre2 />} path="/genre2" /> + <Route element={<Login />} path="/" /> <Route element={<Demo />} path="/demo" /> <Route element={<Single />} path="/single/:theid" /> - <Route element={<Genre />} path="/genre" /> + <Route element={<HomeUser />} path="/homeuser" /> <Route element={<UserProfile />} path="/userProfile" /> {/* ✅ nueva */} <Route element={<SavedSongs />} path="/savedSongs" /> {/* ✅ nueva */} <Route element={<SavedArtists />} path="/savedArtists" /> {/* ✅ nueva */} diff --git a/src/front/js/pages/genre.js b/src/front/js/pages/HomeUser.js similarity index 98% rename from src/front/js/pages/genre.js rename to src/front/js/pages/HomeUser.js index 6a53e12cc2..cc7dae013f 100644 --- a/src/front/js/pages/genre.js +++ b/src/front/js/pages/HomeUser.js @@ -3,7 +3,7 @@ import { useContext } from 'react'; import { Context } from "../store/appContext"; import "../../styles/genre.css" -export const Genre = () => { +export const HomeUser = () => { const { store, actions } = useContext(Context); diff --git a/src/front/js/pages/home.js b/src/front/js/pages/genre2.js similarity index 95% rename from src/front/js/pages/home.js rename to src/front/js/pages/genre2.js index 7b0be46a1e..3ec417545f 100755 --- a/src/front/js/pages/home.js +++ b/src/front/js/pages/genre2.js @@ -3,7 +3,7 @@ import { Context } from "../store/appContext"; import rigoImageUrl from "../../img/rigo-baby.jpg"; import "../../styles/home.css"; -export const Home = () => { +export const Genre2 = () => { const { store, actions } = useContext(Context); return ( diff --git a/src/front/js/pages/login.js b/src/front/js/pages/login.js index 043755db23..aa0c7a7a25 100644 --- a/src/front/js/pages/login.js +++ b/src/front/js/pages/login.js @@ -10,89 +10,110 @@ export const Login = () => { //Estado para almacenar datos del formulario de registro - const[formulario, setFormulario] = useState({ - fullName:"", - username:"", - email:"", - address:"", - password:"", - confirmPassword:"", + const [formulario, setFormulario] = useState({ + fullName: "", + username: "", + email: "", + address: "", + password: "", + confirmPassword: "", + isArtist: false, }); // Estados para manejar el login const [loginData, setLoginData] = useState({ - username:"", - password:"", + username: "", + password: "", }); // Manejar los cambios en los inputs del registro const handleChange = (e) => { - setFormulario({...formulario, [e.target.name]: e.target.value}); + if (e.target.type === "checkbox") { + setFormulario({ + ...formulario, + [e.target.name]: e.target.checked, // Para checkbox, usamos checked + }); + } else { + setFormulario({ ...formulario, [e.target.name]: e.target.value }); + } }; // Manejar cambios en los inpusts del login const handleLoginChange = (e) => { - console.log("cambir campos: ", e.target.name, "Nuevo valor:", e.target.value); - setLoginData({...loginData, [e.target.name]:e.target.value}); + console.log("cambiar campos: ", e.target.name, "Nuevo valor:", e.target.value); + setLoginData({ ...loginData, [e.target.name]: e.target.value }); }; - // Enviar formulario de registro - const handleRegister = async (e) =>{ + const handleRegister = async (e) => { e.preventDefault(); - - if (formulario.password !== formulario.confirmPassword){ + + if (formulario.password !== formulario.confirmPassword) { alert("Las contraseñas no coinciden"); return; } - - console.log("Datos del formulario de registro:", formulario); // Verifica que los datos sean correctos antes de enviarlos - try{ - const response = await fetch("https://fluffy-chainsaw-v6qq999gg9xwfx99v-3001.app.github.dev/api/register",{ - method:"POST", - headers:{"Content-type":"application/json"}, - body:JSON.stringify(formulario), + // Aseguramos que isArtist sea false si no está marcado + const dataToSend = { + ...formulario, + isArtist: formulario.isArtist || false, // Si no está marcado, será false + }; + + console.log("Datos del formulario de registro:", dataToSend); // Verifica que los datos sean correctos antes de enviarlos + + try { + const response = await fetch("https://ideal-space-bassoon-jjqqvvv4q5g4hqpjr-3001.app.github.dev/api/register", { + method: "POST", + headers: { "Content-type": "application/json" }, + body: JSON.stringify(dataToSend), }); const data = await response.json(); - if(response.ok){ - alert("Usuario registrado con exito"); + if (response.ok) { + alert("Usuario registrado con éxito"); setShowRegister(false); setShowLogin(true); - }else{ - alert(data.message || "Error al registrar usuario") + } else { + alert(data.message || "Error al registrar usuario"); } - } catch(error){ - alert("Error en el servidor") + } catch (error) { + alert("Error en el servidor"); } }; - // Enviar formulario de inicio de sesion - const handleLogin = async (e) =>{ + // Enviar formulario de inicio de sesión + const handleLogin = async (e) => { e.preventDefault(); - - try{ - const response = await fetch("https://fluffy-chainsaw-v6qq999gg9xwfx99v-3001.app.github.dev/login", { - method:"POST", - headers:{"Content-Type":"application/json"}, - body: JSON.stringify(loginData) + try { + const response = await fetch("https://ideal-space-bassoon-jjqqvvv4q5g4hqpjr-3001.app.github.dev/api/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(loginData), }); + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || "Error en el inicio de sesión"); + } + const data = await response.json(); + console.log("Respuesta del servidor:", data); - if(response.ok){ - alert("Inicio de sesion correcto"); - localStorage.setItem("Token", data.token); // Guarda el token en local - localStorage.setItem("user", JSON.stringify(data.user)) // guarda los datos del usuario - // aqui puedes redirir a otra pagina - } else{ - alert(data.message || "Error al iniciar sesion") + // Redirigir a la URL proporcionada en la respuesta del servidor + if (data.redirect_url) { + window.location.href = data.redirect_url; } - }catch (error){ - alert("Error en el servidor") + + // Guardar token y datos del usuario + localStorage.setItem("Token", data.access_token); + localStorage.setItem("user", JSON.stringify(data.user)); + + } catch (error) { + console.error("Error al hacer fetch:", error); + alert(error.message || "Error en el servidor"); } - } + }; + return ( <div @@ -139,170 +160,180 @@ export const Login = () => { {/* Formulario de inicio de sesión */} {showLogin && !showForgoten && ( <form onSubmit={handleLogin}> - <h2 className="text-center mb-4">Iniciar sesión</h2> - <div className="mb-3"> - <input - type="text" - className="form-control form-control-lg" - placeholder="Nombre de usuario" - name = "username" - value={loginData.username} - onChange={handleLoginChange} - required - /> - </div> - <div className="mb-3"> - <input - type="password" - className="form-control form-control-lg" - placeholder="Contraseña" - name = "password" - value={loginData.password} - onChange={handleLoginChange} - required - /> - </div> - <button type="submit" className="btn btn-danger w-100 py-2"> - Ingresar - </button> - <div className="text-center mt-2"> - <a href="#" className="text-decoration-none" onClick={() => - setShowForgoten(true) - }>¿He olvidado mi contraseña?</a> - <button - type="button" - className="btn btn-link w-100 mt-2" - onClick={() => { - setShowForgoten(false); - setShowLogin(false); - setShowRegister(false) - }} - > - Volver + <h2 className="text-center mb-4">Iniciar sesión</h2> + <div className="mb-3"> + <input + type="text" + className="form-control form-control-lg" + placeholder="Nombre de usuario" + name="username" + value={loginData.username} + onChange={handleLoginChange} + required + /> + </div> + <div className="mb-3"> + <input + type="password" + className="form-control form-control-lg" + placeholder="Contraseña" + name="password" + value={loginData.password} + onChange={handleLoginChange} + required + /> + </div> + <button type="submit" className="btn btn-danger w-100 py-2"> + Ingresar </button> - </div> - </form > + <div className="text-center mt-2"> + <a href="#" className="text-decoration-none" onClick={() => + setShowForgoten(true) + }>¿He olvidado mi contraseña?</a> + <button + type="button" + className="btn btn-link w-100 mt-2" + onClick={() => { + setShowForgoten(false); + setShowLogin(false); + setShowRegister(false) + }} + > + Volver + </button> + </div> + </form > )} {/* Formulario de registro */} {showRegister && ( <form onSubmit={handleRegister}> - <h2 className="text-center mb-4">Registro</h2> - <div className="mb-3"> - <input - type="text" - className="form-control form-control-lg" - placeholder="Nombre y apellidos" - name="fullName" - value={formulario.fullName} - onChange={handleChange} - required - /> - </div> - <div className="mb-3"> - <input - type="text" - className="form-control form-control-lg" - placeholder="Nombre de usuario" - name="username" - value={formulario.username} - onChange={handleChange} - required - /> - </div> - <div className="mb-3"> - <input - type="email" - className="form-control form-control-lg" - placeholder="Correo electrónico" - name = "email" - value={formulario.email} - onChange={handleChange} - required - /> - </div> - <div className="mb-3"> - <input - type="text" - className="form-control form-control-lg" - placeholder="Dirección" - name = "address" - value={formulario.address} - onChange={handleChange} - required - /> - </div> - <div className="mb-3"> - <input - type="password" - className="form-control form-control-lg" - placeholder="Contraseña" - name = "password" - value={formulario.password} - onChange={handleChange} - required - /> - </div> - <div className="mb-3"> - <input - type="password" - className="form-control form-control-lg" - placeholder="Confirmar contraseña" - name = "confirmPassword" - value={formulario.confirmPassword} - onChange={handleChange} - required - /> - </div> - <div className="mb-3"> - <label className="form-label">Subir foto de perfil</label> - <input type="file" className="form-control form-control-lg" /> - </div> - <button type="submit" className="btn btn-success w-100 py-2"> - Registrarse - </button> - <button - type="button" - className="btn btn-link w-100 mt-2" - onClick={() => setShowRegister(false)} - > - Volver - </button> - </form> + <h2 className="text-center mb-4">Registro</h2> + <div className="mb-3"> + <input + type="text" + className="form-control form-control-lg" + placeholder="Nombre y apellidos" + name="fullName" + value={formulario.fullName} + onChange={handleChange} + required + /> + </div> + <div className="mb-3"> + <input + type="text" + className="form-control form-control-lg" + placeholder="Nombre de usuario" + name="username" + value={formulario.username} + onChange={handleChange} + required + /> + </div> + <div className="mb-3"> + <input + type="email" + className="form-control form-control-lg" + placeholder="Correo electrónico" + name="email" + value={formulario.email} + onChange={handleChange} + required + /> + </div> + <div className="mb-3"> + <input + type="text" + className="form-control form-control-lg" + placeholder="Dirección" + name="address" + value={formulario.address} + onChange={handleChange} + required + /> + </div> + <div className="mb-3"> + <input + type="password" + className="form-control form-control-lg" + placeholder="Contraseña" + name="password" + value={formulario.password} + onChange={handleChange} + required + /> + </div> + <div className="mb-3"> + <input + type="password" + className="form-control form-control-lg" + placeholder="Confirmar contraseña" + name="confirmPassword" + value={formulario.confirmPassword} + onChange={handleChange} + required + /> + </div> + <div className="mb-3"> + <label className="form-label me-2">¿Eres artista?</label> + <input + type="checkbox" + className="form-check-input" + name="isArtist" + checked={formulario.isArtist} // Se gestiona el estado de este checkbox + onChange={handleChange} + /> + </div> + <div className="mb-3"> + <label className="form-label">Subir foto de perfil</label> + <input type="file" className="form-control form-control-lg" /> + </div> + <button type="submit" className="btn btn-success w-100 py-2"> + Registrarse + </button> + <button + type="button" + className="btn btn-link w-100 mt-2" + onClick={() => setShowRegister(false)} + > + Volver + </button> + </form> )} {/* Formulario de recuperar contraseña*/} {showForgoten && ( <form onSubmit={handleRegister}> - <h2 className="text-center mb-4">¿He olvidado mi contraseña?</h2> - <div className="mb-3"> - <input - type="text" - className="form-control form-control-lg" - placeholder="Nomber de usuario" - required - /> - </div> - <div className="mb-3"> - <input - type="password" - className="form-control form-control-lg" - placeholder="Nueva contraseña" - required - /> - </div> - <button - type="button" - className="btn btn-link w-100 mt-2" - onClick={() => { - setShowLogin(true); - setShowRegister(false); - setShowForgoten(false) - }} - > - Volver - </button> - </form> - )} + <h2 className="text-center mb-4">¿He olvidado mi contraseña?</h2> + <div className="mb-3"> + <input + type="text" + className="form-control form-control-lg" + placeholder="Nomber de usuario" + required + /> + </div> + <div className="mb-3"> + <input + type="password" + className="form-control form-control-lg" + placeholder="Nueva contraseña" + required + /> + </div> + <button + type="button" + className="btn btn-link w-100 mt-2" + onClick={() => { + setShowLogin(true); + setShowRegister(false); + setShowForgoten(false) + }} + > + Volver + </button> + </form> + )} </div> From a51f2a8979c85ee3cfba7ab5789ea7ea2b23269b Mon Sep 17 00:00:00 2001 From: Oscar Parra <oscarparrapalomino@gmail.com> Date: Mon, 10 Mar 2025 16:35:04 +0000 Subject: [PATCH 24/25] logindefinitivo --- src/front/js/component/navbar.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/front/js/component/navbar.js b/src/front/js/component/navbar.js index 8837c3fcdc..addaf673b1 100755 --- a/src/front/js/component/navbar.js +++ b/src/front/js/component/navbar.js @@ -23,7 +23,8 @@ export const Navbar = () => { {/* Menú desplegable */} <div className={`dropdown-menu ${menuOpen ? "show" : ""}`}> <Link to="/userProfile" className="dropdown-item" onClick={() => setMenuOpen(false)}>Perfil</Link> - <Link to="/logout" className="dropdown-item" onClick={() => setMenuOpen(false)}>Logout</Link> + <Link to="/userdata" className="dropdown-item" onClick={() => setMenuOpen(false)}>Datos</Link> + <Link to="/" className="dropdown-item" onClick={() => setMenuOpen(false)}>Logout</Link> </div> </div> </div> From 45fd4ded7ce5f2f4311cfa5fbf77f163acd8c2a7 Mon Sep 17 00:00:00 2001 From: Oscar Parra <oscarparrapalomino@gmail.com> Date: Mon, 10 Mar 2025 18:31:08 +0000 Subject: [PATCH 25/25] espero que vaya --- src/api/admin.py | 6 +++--- src/api/models.py | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/api/admin.py b/src/api/admin.py index 58410e318a..21e5630954 100644 --- a/src/api/admin.py +++ b/src/api/admin.py @@ -1,7 +1,7 @@ import os from flask_admin import Admin -from .models import db, User, Genre, ArtistProfile,Photo,Video,Music,SavedMusic,FollowArtist +from .models import db, User, Genre, ArtistProfile,Photo,Video,Song,SavedSong,FollowArtist from flask_admin.contrib.sqla import ModelView def setup_admin(app): @@ -15,8 +15,8 @@ def setup_admin(app): admin.add_view(ModelView(Genre, db.session)) admin.add_view(ModelView(Photo, db.session)) admin.add_view(ModelView(Video, db.session)) - admin.add_view(ModelView(Music, db.session)) - admin.add_view(ModelView(SavedMusic, db.session)) + admin.add_view(ModelView(Song, db.session)) + admin.add_view(ModelView(SavedSong, db.session)) admin.add_view(ModelView(FollowArtist, db.session)) admin.add_view(ModelView(ArtistProfile, db.session)) diff --git a/src/api/models.py b/src/api/models.py index 9c064d348c..36f4132e69 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -61,7 +61,6 @@ class ArtistProfile(db.Model): artist_photos = db.relationship("Photo", backref="artist_profile") artist_videos = db.relationship("Video", backref="artist_profile") - artist_music = db.relationship("Music", backref="artist_profile") artist_songs = db.relationship("Song", backref="artist_profile") def __repr__(self):