diff --git a/.canvas b/.canvas deleted file mode 100644 index 9c0f654da..000000000 --- a/.canvas +++ /dev/null @@ -1,10 +0,0 @@ ---- -:lessons: -- :id: 255345 - :course_id: 6130 - :canvas_url: https://learning.flatironschool.com/courses/6130/pages/phase-4-full-stack-application-project-template - :type: page -- :id: 297405 - :course_id: 7560 - :canvas_url: https://learning.flatironschool.com/courses/7560/pages/phase-4-full-stack-application-project-template - :type: page diff --git a/.gitignore b/.gitignore index 9a787a2da..f852b0491 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,5 @@ __pycache__ instance .vscode package-lock.json + +.env diff --git a/Pipfile b/Pipfile index 9c0ba76e7..fd5702b1b 100644 --- a/Pipfile +++ b/Pipfile @@ -13,6 +13,8 @@ sqlalchemy-serializer = "*" flask-restful = "*" flask-cors = "*" faker = "*" +flask-bcrypt = "*" +python-dotenv = "*" [requires] python_full_version = "3.8.13" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 000000000..5c587edcb --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,587 @@ +{ + "_meta": { + "hash": { + "sha256": "35c75edc37a0089159c707dbbe7f89fc8f906ad499bf68767c192d6094d4ded1" + }, + "pipfile-spec": 6, + "requires": { + "python_full_version": "3.8.13" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "alembic": { + "hashes": [ + "sha256:1ff0ae32975f4fd96028c39ed9bb3c867fe3af956bd7bb37343b54c9fe7445ef", + "sha256:6b8733129a6224a9a711e17c99b08462dbf7cc9670ba8f2e2ae9af860ceb1953" + ], + "markers": "python_version >= '3.8'", + "version": "==1.13.2" + }, + "aniso8601": { + "hashes": [ + "sha256:1d2b7ef82963909e93c4f24ce48d4de9e66009a21bf1c1e1c85bdd0812fe412f", + "sha256:72e3117667eedf66951bb2d93f4296a56b94b078a8a95905a052611fb3f1b973" + ], + "version": "==9.0.1" + }, + "appnope": { + "hashes": [ + "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", + "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c" + ], + "markers": "sys_platform == 'darwin'", + "version": "==0.1.4" + }, + "asttokens": { + "hashes": [ + "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24", + "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0" + ], + "version": "==2.4.1" + }, + "backcall": { + "hashes": [ + "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e", + "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255" + ], + "version": "==0.2.0" + }, + "bcrypt": { + "hashes": [ + "sha256:096a15d26ed6ce37a14c1ac1e48119660f21b24cba457f160a4b830f3fe6b5cb", + "sha256:0da52759f7f30e83f1e30a888d9163a81353ef224d82dc58eb5bb52efcabc399", + "sha256:1bb429fedbe0249465cdd85a58e8376f31bb315e484f16e68ca4c786dcc04291", + "sha256:1d84cf6d877918620b687b8fd1bf7781d11e8a0998f576c7aa939776b512b98d", + "sha256:1ee38e858bf5d0287c39b7a1fc59eec64bbf880c7d504d3a06a96c16e14058e7", + "sha256:1ff39b78a52cf03fdf902635e4c81e544714861ba3f0efc56558979dd4f09170", + "sha256:27fe0f57bb5573104b5a6de5e4153c60814c711b29364c10a75a54bb6d7ff48d", + "sha256:3413bd60460f76097ee2e0a493ccebe4a7601918219c02f503984f0a7ee0aebe", + "sha256:3698393a1b1f1fd5714524193849d0c6d524d33523acca37cd28f02899285060", + "sha256:373db9abe198e8e2c70d12b479464e0d5092cc122b20ec504097b5f2297ed184", + "sha256:39e1d30c7233cfc54f5c3f2c825156fe044efdd3e0b9d309512cc514a263ec2a", + "sha256:3bbbfb2734f0e4f37c5136130405332640a1e46e6b23e000eeff2ba8d005da68", + "sha256:3d3a6d28cb2305b43feac298774b997e372e56c7c7afd90a12b3dc49b189151c", + "sha256:5a1e8aa9b28ae28020a3ac4b053117fb51c57a010b9f969603ed885f23841458", + "sha256:61ed14326ee023917ecd093ee6ef422a72f3aec6f07e21ea5f10622b735538a9", + "sha256:655ea221910bcac76ea08aaa76df427ef8625f92e55a8ee44fbf7753dbabb328", + "sha256:762a2c5fb35f89606a9fde5e51392dad0cd1ab7ae64149a8b935fe8d79dd5ed7", + "sha256:77800b7147c9dc905db1cba26abe31e504d8247ac73580b4aa179f98e6608f34", + "sha256:8ac68872c82f1add6a20bd489870c71b00ebacd2e9134a8aa3f98a0052ab4b0e", + "sha256:8d7bb9c42801035e61c109c345a28ed7e84426ae4865511eb82e913df18f58c2", + "sha256:8f6ede91359e5df88d1f5c1ef47428a4420136f3ce97763e31b86dd8280fbdf5", + "sha256:9c1c4ad86351339c5f320ca372dfba6cb6beb25e8efc659bedd918d921956bae", + "sha256:c02d944ca89d9b1922ceb8a46460dd17df1ba37ab66feac4870f6862a1533c00", + "sha256:c52aac18ea1f4a4f65963ea4f9530c306b56ccd0c6f8c8da0c06976e34a6e841", + "sha256:cb2a8ec2bc07d3553ccebf0746bbf3d19426d1c6d1adbd4fa48925f66af7b9e8", + "sha256:cf69eaf5185fd58f268f805b505ce31f9b9fc2d64b376642164e9244540c1221", + "sha256:f4f4acf526fcd1c34e7ce851147deedd4e26e6402369304220250598b26448db" + ], + "markers": "python_version >= '3.7'", + "version": "==4.2.0" + }, + "click": { + "hashes": [ + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.7" + }, + "decorator": { + "hashes": [ + "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330", + "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186" + ], + "markers": "python_version >= '3.7'", + "version": "==5.1.1" + }, + "executing": { + "hashes": [ + "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147", + "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc" + ], + "markers": "python_version >= '3.5'", + "version": "==2.0.1" + }, + "faker": { + "hashes": [ + "sha256:32c78b68d2ba97aaad78422e4035785de2b4bb46b81e428190fc11978da9036c", + "sha256:55ed0c4ed7bf16800c64823805f6fbbe6d4823db4b7c0903f6f890b8e4d6c34b" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==27.0.0" + }, + "flask": { + "hashes": [ + "sha256:58107ed83443e86067e41eff4631b058178191a355886f8e479e347fa1285fdf", + "sha256:edee9b0a7ff26621bd5a8c10ff484ae28737a2410d99b0bb9a6850c7fb977aa0" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==2.2.5" + }, + "flask-bcrypt": { + "hashes": [ + "sha256:062fd991dc9118d05ac0583675507b9fe4670e44416c97e0e6819d03d01f808a", + "sha256:f07b66b811417ea64eb188ae6455b0b708a793d966e1a80ceec4a23bc42a4369" + ], + "index": "pypi", + "version": "==1.0.1" + }, + "flask-cors": { + "hashes": [ + "sha256:eeb69b342142fdbf4766ad99357a7f3876a2ceb77689dc10ff912aac06c389e4", + "sha256:f2a704e4458665580c074b714c4627dd5a306b333deb9074d0b1794dfa2fb677" + ], + "index": "pypi", + "version": "==4.0.1" + }, + "flask-migrate": { + "hashes": [ + "sha256:5c532be17e7b43a223b7500d620edae33795df27c75811ddf32560f7d48ec617", + "sha256:dff7dd25113c210b069af280ea713b883f3840c1e3455274745d7355778c8622" + ], + "index": "pypi", + "markers": "python_version >= '3.6'", + "version": "==4.0.7" + }, + "flask-restful": { + "hashes": [ + "sha256:1cf93c535172f112e080b0d4503a8d15f93a48c88bdd36dd87269bdaf405051b", + "sha256:fe4af2ef0027df8f9b4f797aba20c5566801b6ade995ac63b588abf1a59cec37" + ], + "index": "pypi", + "version": "==0.3.10" + }, + "flask-sqlalchemy": { + "hashes": [ + "sha256:2764335f3c9d7ebdc9ed6044afaf98aae9fa50d7a074cef55dde307ec95903ec", + "sha256:add5750b2f9cd10512995261ee2aa23fab85bd5626061aa3c564b33bb4aa780a" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==3.0.3" + }, + "greenlet": { + "hashes": [ + "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67", + "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6", + "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257", + "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4", + "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676", + "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61", + "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc", + "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca", + "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7", + "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728", + "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305", + "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6", + "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379", + "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414", + "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04", + "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a", + "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf", + "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491", + "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559", + "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e", + "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274", + "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb", + "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b", + "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9", + "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b", + "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be", + "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506", + "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405", + "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113", + "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f", + "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5", + "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230", + "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d", + "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f", + "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a", + "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e", + "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61", + "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6", + "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d", + "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71", + "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22", + "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2", + "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3", + "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067", + "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc", + "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881", + "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3", + "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e", + "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac", + "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53", + "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0", + "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b", + "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83", + "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41", + "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c", + "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf", + "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da", + "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33" + ], + "markers": "python_version < '3.13' 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.0.3" + }, + "importlib-metadata": { + "hashes": [ + "sha256:11901fa0c2f97919b288679932bb64febaeacf289d18ac84dd68cb2e74213369", + "sha256:72e8d4399996132204f9a16dcc751af254a48f8d1b20b9ff0f98d4a8f901e73d" + ], + "markers": "python_version < '3.10'", + "version": "==8.2.0" + }, + "importlib-resources": { + "hashes": [ + "sha256:6cbfbefc449cc6e2095dd184691b7a12a04f40bc75dd4c55d31c34f174cdf57a", + "sha256:8bba8c54a8a3afaa1419910845fa26ebd706dc716dd208d9b158b4b6966f5c5c" + ], + "markers": "python_version < '3.9'", + "version": "==6.4.2" + }, + "ipdb": { + "hashes": [ + "sha256:951bd9a64731c444fd907a5ce268543020086a697f6be08f7cc2c9a752a278c5" + ], + "index": "pypi", + "markers": "python_version >= '2.7'", + "version": "==0.13.9" + }, + "ipython": { + "hashes": [ + "sha256:3910c4b54543c2ad73d06579aa771041b7d5707b033bd488669b4cf544e3b363", + "sha256:b0340d46a933d27c657b211a329d0be23793c36595acf9e6ef4164bc01a1804c" + ], + "markers": "python_version >= '3.7'", + "version": "==8.12.3" + }, + "itsdangerous": { + "hashes": [ + "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", + "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173" + ], + "markers": "python_version >= '3.8'", + "version": "==2.2.0" + }, + "jedi": { + "hashes": [ + "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd", + "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0" + ], + "markers": "python_version >= '3.6'", + "version": "==0.19.1" + }, + "jinja2": { + "hashes": [ + "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", + "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.4" + }, + "mako": { + "hashes": [ + "sha256:260f1dbc3a519453a9c856dedfe4beb4e50bd5a26d96386cb6c80856556bb91a", + "sha256:48dbc20568c1d276a2698b36d968fa76161bf127194907ea6fc594fa81f943bc" + ], + "markers": "python_version >= '3.8'", + "version": "==1.3.5" + }, + "markupsafe": { + "hashes": [ + "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", + "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", + "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", + "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", + "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", + "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", + "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", + "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df", + "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", + "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", + "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", + "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", + "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", + "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371", + "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2", + "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", + "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52", + "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", + "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", + "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", + "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", + "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", + "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", + "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", + "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", + "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", + "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", + "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", + "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", + "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9", + "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", + "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", + "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", + "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", + "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", + "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", + "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a", + "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", + "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", + "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", + "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", + "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", + "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", + "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", + "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", + "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f", + "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50", + "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", + "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", + "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", + "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", + "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", + "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", + "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", + "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf", + "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", + "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", + "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", + "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", + "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68" + ], + "markers": "python_version >= '3.7'", + "version": "==2.1.5" + }, + "matplotlib-inline": { + "hashes": [ + "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", + "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca" + ], + "markers": "python_version >= '3.8'", + "version": "==0.1.7" + }, + "parso": { + "hashes": [ + "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", + "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d" + ], + "markers": "python_version >= '3.6'", + "version": "==0.8.4" + }, + "pexpect": { + "hashes": [ + "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", + "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f" + ], + "markers": "sys_platform != 'win32'", + "version": "==4.9.0" + }, + "pickleshare": { + "hashes": [ + "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", + "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56" + ], + "version": "==0.7.5" + }, + "prompt-toolkit": { + "hashes": [ + "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10", + "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.0.47" + }, + "ptyprocess": { + "hashes": [ + "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", + "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220" + ], + "version": "==0.7.0" + }, + "pure-eval": { + "hashes": [ + "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", + "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42" + ], + "version": "==0.2.3" + }, + "pygments": { + "hashes": [ + "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", + "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a" + ], + "markers": "python_version >= '3.8'", + "version": "==2.18.0" + }, + "python-dateutil": { + "hashes": [ + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.9.0.post0" + }, + "python-dotenv": { + "hashes": [ + "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", + "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.0.1" + }, + "pytz": { + "hashes": [ + "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812", + "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319" + ], + "version": "==2024.1" + }, + "setuptools": { + "hashes": [ + "sha256:80aacbf633704e9c8bfa1d99fa5dd4dc59573efcf9e4042c13d3bcef91ac2ef9", + "sha256:f11dd94b7bae3a156a95ec151f24e4637fb4fa19c878e4d191bfb8b2d82728c4" + ], + "markers": "python_version >= '3.8'", + "version": "==72.2.0" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + }, + "sqlalchemy": { + "hashes": [ + "sha256:01438ebcdc566d58c93af0171c74ec28efe6a29184b773e378a385e6215389da", + "sha256:0c1c9b673d21477cec17ab10bc4decb1322843ba35b481585facd88203754fc5", + "sha256:0c9045ecc2e4db59bfc97b20516dfdf8e41d910ac6fb667ebd3a79ea54084619", + "sha256:0d322cc9c9b2154ba7e82f7bf25ecc7c36fbe2d82e2933b3642fc095a52cfc78", + "sha256:0ef18a84e5116340e38eca3e7f9eeaaef62738891422e7c2a0b80feab165905f", + "sha256:1467940318e4a860afd546ef61fefb98a14d935cd6817ed07a228c7f7c62f389", + "sha256:14e09e083a5796d513918a66f3d6aedbc131e39e80875afe81d98a03312889e6", + "sha256:167e7497035c303ae50651b351c28dc22a40bb98fbdb8468cdc971821b1ae533", + "sha256:19d98f4f58b13900d8dec4ed09dd09ef292208ee44cc9c2fe01c1f0a2fe440e9", + "sha256:21b053be28a8a414f2ddd401f1be8361e41032d2ef5884b2f31d31cb723e559f", + "sha256:251f0d1108aab8ea7b9aadbd07fb47fb8e3a5838dde34aa95a3349876b5a1f1d", + "sha256:295ff8689544f7ee7e819529633d058bd458c1fd7f7e3eebd0f9268ebc56c2a0", + "sha256:2b6be53e4fde0065524f1a0a7929b10e9280987b320716c1509478b712a7688c", + "sha256:306fe44e754a91cd9d600a6b070c1f2fadbb4a1a257b8781ccf33c7067fd3e4d", + "sha256:31983018b74908ebc6c996a16ad3690301a23befb643093fcfe85efd292e384d", + "sha256:328429aecaba2aee3d71e11f2477c14eec5990fb6d0e884107935f7fb6001632", + "sha256:3bd1cae7519283ff525e64645ebd7a3e0283f3c038f461ecc1c7b040a0c932a1", + "sha256:3cd33c61513cb1b7371fd40cf221256456d26a56284e7d19d1f0b9f1eb7dd7e8", + "sha256:3eb6a97a1d39976f360b10ff208c73afb6a4de86dd2a6212ddf65c4a6a2347d5", + "sha256:4363ed245a6231f2e2957cccdda3c776265a75851f4753c60f3004b90e69bfeb", + "sha256:4488120becf9b71b3ac718f4138269a6be99a42fe023ec457896ba4f80749525", + "sha256:49496b68cd190a147118af585173ee624114dfb2e0297558c460ad7495f9dfe2", + "sha256:4979dc80fbbc9d2ef569e71e0896990bc94df2b9fdbd878290bd129b65ab579c", + "sha256:52fec964fba2ef46476312a03ec8c425956b05c20220a1a03703537824b5e8e1", + "sha256:5954463675cb15db8d4b521f3566a017c8789222b8316b1e6934c811018ee08b", + "sha256:62e23d0ac103bcf1c5555b6c88c114089587bc64d048fef5bbdb58dfd26f96da", + "sha256:6bab3db192a0c35e3c9d1560eb8332463e29e5507dbd822e29a0a3c48c0a8d92", + "sha256:6c742be912f57586ac43af38b3848f7688863a403dfb220193a882ea60e1ec3a", + "sha256:723a40ee2cc7ea653645bd4cf024326dea2076673fc9d3d33f20f6c81db83e1d", + "sha256:78c03d0f8a5ab4f3034c0e8482cfcc415a3ec6193491cfa1c643ed707d476f16", + "sha256:7d6ba0497c1d066dd004e0f02a92426ca2df20fac08728d03f67f6960271feec", + "sha256:7dd8583df2f98dea28b5cd53a1beac963f4f9d087888d75f22fcc93a07cf8d84", + "sha256:85a01b5599e790e76ac3fe3aa2f26e1feba56270023d6afd5550ed63c68552b3", + "sha256:8a37e4d265033c897892279e8adf505c8b6b4075f2b40d77afb31f7185cd6ecd", + "sha256:8bd63d051f4f313b102a2af1cbc8b80f061bf78f3d5bd0843ff70b5859e27924", + "sha256:916a798f62f410c0b80b63683c8061f5ebe237b0f4ad778739304253353bc1cb", + "sha256:9365a3da32dabd3e69e06b972b1ffb0c89668994c7e8e75ce21d3e5e69ddef28", + "sha256:99db65e6f3ab42e06c318f15c98f59a436f1c78179e6a6f40f529c8cc7100b22", + "sha256:aaf04784797dcdf4c0aa952c8d234fa01974c4729db55c45732520ce12dd95b4", + "sha256:acd9b73c5c15f0ec5ce18128b1fe9157ddd0044abc373e6ecd5ba376a7e5d961", + "sha256:ada0102afff4890f651ed91120c1120065663506b760da4e7823913ebd3258be", + "sha256:b178e875a7a25b5938b53b006598ee7645172fccafe1c291a706e93f48499ff5", + "sha256:b27dfb676ac02529fb6e343b3a482303f16e6bc3a4d868b73935b8792edb52d0", + "sha256:b8afd5b26570bf41c35c0121801479958b4446751a3971fb9a480c1afd85558e", + "sha256:bf2360a5e0f7bd75fa80431bf8ebcfb920c9f885e7956c7efde89031695cafb8", + "sha256:c1b88cc8b02b6a5f0efb0345a03672d4c897dc7d92585176f88c67346f565ea8", + "sha256:c41a2b9ca80ee555decc605bd3c4520cc6fef9abde8fd66b1cf65126a6922d65", + "sha256:c750987fc876813f27b60d619b987b057eb4896b81117f73bb8d9918c14f1cad", + "sha256:e567a8793a692451f706b363ccf3c45e056b67d90ead58c3bc9471af5d212202" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.32" + }, + "sqlalchemy-serializer": { + "hashes": [ + "sha256:5e1f83fc6d8a4f7618100c1b9a6af949498210756b974527ec3c8c1ec7e1300f" + ], + "index": "pypi", + "version": "==1.4.12" + }, + "stack-data": { + "hashes": [ + "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", + "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695" + ], + "version": "==0.6.3" + }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "markers": "python_version >= '3.7'", + "version": "==0.10.2" + }, + "traitlets": { + "hashes": [ + "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", + "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f" + ], + "markers": "python_version >= '3.8'", + "version": "==5.14.3" + }, + "typing-extensions": { + "hashes": [ + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" + ], + "markers": "python_version >= '3.8'", + "version": "==4.12.2" + }, + "wcwidth": { + "hashes": [ + "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", + "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5" + ], + "version": "==0.2.13" + }, + "werkzeug": { + "hashes": [ + "sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f", + "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==2.2.2" + }, + "zipp": { + "hashes": [ + "sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31", + "sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d" + ], + "markers": "python_version >= '3.8'", + "version": "==3.20.0" + } + }, + "develop": {} +} diff --git a/client/package.json b/client/package.json index 2e4db7320..a1cb6e200 100644 --- a/client/package.json +++ b/client/package.json @@ -7,13 +7,17 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "ajv": "^8.17.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^5.2.0", "react-scripts": "5.0.1", + "recharts": "^2.12.7", "web-vitals": "^2.1.4" }, - "devDependencies": {"@babel/plugin-proposal-private-property-in-object":"^7.21.11"}, + "devDependencies": { + "@babel/plugin-proposal-private-property-in-object": "^7.21.11" + }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", diff --git a/client/public/index.html b/client/public/index.html index a7bfc838f..242a2776a 100644 --- a/client/public/index.html +++ b/client/public/index.html @@ -29,7 +29,7 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - Reciplease + FlatPay diff --git a/client/src/Routes.js b/client/src/Routes.js new file mode 100644 index 000000000..2fbe262e5 --- /dev/null +++ b/client/src/Routes.js @@ -0,0 +1,33 @@ +import React from 'react'; +import { Switch, Route, Redirect } from 'react-router-dom'; +import Transactions from './components/Transaction/Transactions'; +import CreditPage from './pages/CreditPage'; +import DebitPage from './pages/DebitPage'; +import StatsPage from './pages/StatsPage'; +import FriendshipPage from './pages/FriendshipPage'; + +function Routes({ currentUser }) { + if (!currentUser) return ; + + return ( + + + + + + + + + + + + + + + + + + ); +} + +export default Routes; \ No newline at end of file diff --git a/client/src/components/App.js b/client/src/components/App.js index af444c5b1..37f6cec52 100644 --- a/client/src/components/App.js +++ b/client/src/components/App.js @@ -1,8 +1,54 @@ -import React, { useEffect, useState } from "react"; -import { Switch, Route } from "react-router-dom"; +import React, { useState, useEffect } from 'react'; +import { BrowserRouter as Router, Switch, Route, Redirect } from 'react-router-dom'; +import NavBar from './NavBar'; +import UserPanel from './UserPanel/UserPanel'; +import CreditPage from '../pages/CreditPage'; +import DebitPage from '../pages/DebitPage'; +import StatsPage from '../pages/StatsPage'; +import FriendshipPage from '../pages/FriendshipPage'; function App() { - return

Project Client

; + const [currentUser, setCurrentUser] = useState(null); + + useEffect(() => { + fetch('/check_session') + .then(res => { + if (res.status === 200) { + res.json().then(data => setCurrentUser(data)); + } + }); + }, []); + + return ( + +
+

FlatPay

+ + + + + + {currentUser && ( + <> + + + + + + + + + + + + + + )} + + +
+
+ ); } -export default App; +export default App; \ No newline at end of file diff --git a/client/src/components/NavBar.js b/client/src/components/NavBar.js new file mode 100644 index 000000000..f71ca015a --- /dev/null +++ b/client/src/components/NavBar.js @@ -0,0 +1,29 @@ +import React from 'react'; +import { Link, useHistory } from 'react-router-dom'; + +function NavBar({ currentUser, setCurrentUser }) { + const history = useHistory(); + + const handleLogout = () => { + fetch('/logout', { method: 'DELETE' }) + .then(() => { + setCurrentUser(null); + history.push('/'); + }); + }; + + if (!currentUser) return null; + + return ( + + ); +} + +export default NavBar; \ No newline at end of file diff --git a/client/src/components/Transaction/Payment.js b/client/src/components/Transaction/Payment.js new file mode 100644 index 000000000..78dadd917 --- /dev/null +++ b/client/src/components/Transaction/Payment.js @@ -0,0 +1,7 @@ +import React from 'react' + +export default function Payment() { + return ( +
Send Payment
+ ) +} diff --git a/client/src/components/Transaction/TransactionRequest.js b/client/src/components/Transaction/TransactionRequest.js new file mode 100644 index 000000000..62c744d72 --- /dev/null +++ b/client/src/components/Transaction/TransactionRequest.js @@ -0,0 +1,93 @@ +import React, { useState, useEffect } from 'react'; + +function TransactionRequest({ createRequest, currentUser }) { + + const [amount, setAmount] = useState(''); + const [selectedUser, setSelectedUser] = useState(''); + const [year, setYear] = useState(new Date().getFullYear()); + const [users, setUsers] = useState([]); + const [userRole, setUserRole] = useState('sender'); + + + useEffect(() => { + fetch('/users') + + .then(res => res.json()) + .then(data => setUsers(data.filter(user => user.id !== currentUser.id))); + }, [currentUser.id]); + + + function handleSubmit(e) { + e.preventDefault(); + const requestData = { + amount: parseFloat(amount), + year: parseInt(year), + [userRole === 'sender' ? 'requestee' : 'requestor']: parseInt(selectedUser), + [userRole === 'sender' ? 'requestor' : 'requestee']: currentUser.id + }; + createRequest(requestData); + setAmount(''); + setSelectedUser(''); + setYear(new Date().getFullYear()); + } + + return ( +
+

New Transaction:

+ +
+ + +
+ + + + + setAmount(e.target.value)} + placeholder="Amount" + required + /> + + setYear(e.target.value)} + placeholder="Year" + min="1900" + max="2099" + step="1" + required + /> + + +
+ ); +} + +export default TransactionRequest; diff --git a/client/src/components/Transaction/Transactions.js b/client/src/components/Transaction/Transactions.js new file mode 100644 index 000000000..1314d9d71 --- /dev/null +++ b/client/src/components/Transaction/Transactions.js @@ -0,0 +1,127 @@ + +import React, { useState, useEffect } from 'react'; +import TransactionRequest from './TransactionRequest'; +import Payment from './Payment'; + + +export default function Transactions({ currentUser }) { + const [debits, setDebits] = useState([]); + const [credits, setCredits] = useState([]); + + // Function to fetch debits from the server + const fetchDebits = () => { + fetch('/debits') + .then(res => { + if (res.status === 200) { + return res.json(); + } + }) + .then(data => setDebits(data)); + }; + + // Function to fetch credits from the server + const fetchCredits = () => { + fetch('/credits') + .then(res => { + if (res.status === 200) { + return res.json(); + } + }) + .then(data => setCredits(data)); + }; + + useEffect(() => { + fetchDebits(); + fetchCredits(); + }, []); + + + // Function to create a new transaction request + const createRequest = (content) => { + fetch('/request', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: JSON.stringify(content), + }).then(() => { + fetchDebits(); + fetchCredits(); + }); + }; + + + // Function to handle payment (deleting a transaction) + const handlePayment = (e) => { + e.preventDefault(); + const transaction_to_delete = e.target.parentNode.id; + fetch('/payment', { + method: 'DELETE', + headers: { + 'Content-type': 'application/json', + 'Accept': 'application/json', + }, + body: JSON.stringify({ id: transaction_to_delete }), + }).then((res) => { + if (res.status === 204) { + setDebits((debits) => debits.filter((debit) => debit.id !== parseInt(transaction_to_delete))); + } + }); + }; + + // Implement a handle payment. + // In debit mapping have a button to delete the transaction + // needs a handleDelete or handlePay + + const handlePayment = (e) =>{ + e.preventDefault() + // console.log(e.target.parentNode.id) + const transaction_to_delete = e.target.parentNode.id + fetch('/payment', { + method:'DELETE', + headers:{ + 'Content-type':'application/json', + 'Accept':'application/json' + }, + body:JSON.stringify({id:transaction_to_delete}) + }) + .then( res => { + if(res.status == 204){ + setDebits((debits) => debits.filter((debit) => debit.id !==parseInt(transaction_to_delete))) + } + + }) + + // .then(res => res.json()) + // .then(data => setDebits((data) => data.filter((debit) => debit.id !== e.target.parentNode.id))) + } + + return ( + <> + +
Debits:
+ {debits.map(debit => ( +

+ I owe {debit.requestor_username} ${debit.amount} in {debit.year} + +

+ ))} + +
Credits:
+ {credits.map(credit => ( +

+ {credit.requestee_username} owes me ${credit.amount} in {credit.year} +

+ ))} + + + + + + ); +} + + + + diff --git a/client/src/components/UserPanel/Login.js b/client/src/components/UserPanel/Login.js new file mode 100644 index 000000000..4c4b71db7 --- /dev/null +++ b/client/src/components/UserPanel/Login.js @@ -0,0 +1,55 @@ +import React from 'react' +import { useState } from 'react' + +export default function Login({setCurrentUser}) { + //states + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + + const handleSubmit = (e) =>{ + e.preventDefault() + fetch('/login', { + method:'POST', + headers:{ + 'Content-Type':'application/json', + 'Accept':'application/json' + }, + body:JSON.stringify( + {username, password} + ) + }) + .then(res => { + if (res.ok) { + res.json() + .then(data => setCurrentUser(data)) + } else{ + alert('Invalid username or password') + } + }) + } + + return ( + +
+ +

Login

+ + setUsername(e.target.value)} + value={username} + placeholder='username' + /> + + setPassword(e.target.value)} + value={password} + placeholder='password' + /> + + + +
+ ) +} diff --git a/client/src/components/UserPanel/Signup.js b/client/src/components/UserPanel/Signup.js new file mode 100644 index 000000000..de62284cf --- /dev/null +++ b/client/src/components/UserPanel/Signup.js @@ -0,0 +1,57 @@ +import React from 'react' +import { useState } from 'react' + + +export default function Signup({setCurrentUser}) { + + //states + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + + const handleSubmit = (e) =>{ + e.preventDefault() + fetch('/signup', { + method:'POST', + headers:{ + 'Content-Type':'application/json', + 'Accept':'application/json' + }, + body:JSON.stringify({ + username, password + }) + }) + .then(res =>{ + if (res.ok){ + res.json() + .then( data => setCurrentUser(data)) + } else{ + res.json() + .then( data => alert(data.error)) + } + }) + } + + return ( +
+ +

Signup

+ + setUsername(e.target.value)} + value={username} + placeholder='username' + /> + + setPassword(e.target.value)} + value={password} + placeholder='password' + /> + + + +
+ ) +} diff --git a/client/src/components/UserPanel/UserDetails.js b/client/src/components/UserPanel/UserDetails.js new file mode 100644 index 000000000..526349961 --- /dev/null +++ b/client/src/components/UserPanel/UserDetails.js @@ -0,0 +1,20 @@ +import React from 'react' + +export default function UserDetails({currentUser, setCurrentUser}) { + + const handleLogout = () =>{ + fetch('/logout', { + method:'DELETE' + }) + .then(res => res.json()) + .then(data => console.log(data)) + .catch(error => setCurrentUser(null)) + } + + return ( +
+

Welcome {currentUser.username}

+ +
+ ) +} diff --git a/client/src/components/UserPanel/UserPanel.js b/client/src/components/UserPanel/UserPanel.js new file mode 100644 index 000000000..84c22f79b --- /dev/null +++ b/client/src/components/UserPanel/UserPanel.js @@ -0,0 +1,24 @@ +import React from 'react' +import SignUp from './Signup' +import Login from './Login' +import UserDetails from './UserDetails' +import Transactions from '../Transaction/Transactions' + +export default function UserPanel({currentUser, setCurrentUser}) { + + if (!currentUser){ + return ( +
+ + +
+ ) + } else{ + return ( + <> + + + + ) + } +} diff --git a/client/src/pages/CreditPage.js b/client/src/pages/CreditPage.js new file mode 100644 index 000000000..d0bc5b636 --- /dev/null +++ b/client/src/pages/CreditPage.js @@ -0,0 +1,27 @@ +import React, { useState, useEffect } from 'react'; + +function CreditPage({ currentUser }) { + const [credits, setCredits] = useState([]); + + useEffect(() => { + fetch('/credits') + .then(res => { + if (res.status === 200) { + res.json().then(data => setCredits(data)); + } + }); + }, []); + + return ( +
+

Credit Transactions

+ {credits.map(credit => ( +

+ {credit.requestee_username} owes me ${credit.amount} +

+ ))} +
+ ); +} + +export default CreditPage; \ No newline at end of file diff --git a/client/src/pages/DebitPage.js b/client/src/pages/DebitPage.js new file mode 100644 index 000000000..306bcecf6 --- /dev/null +++ b/client/src/pages/DebitPage.js @@ -0,0 +1,27 @@ +import React, { useState, useEffect } from 'react'; + +function DebitPage({ currentUser }) { + const [debits, setDebits] = useState([]); + + useEffect(() => { + fetch('/debits') + .then(res => { + if (res.status === 200) { + res.json().then(data => setDebits(data)); + } + }); + }, []); + + return ( +
+

Debit Transactions

+ {debits.map(debit => ( +

+ I owe {debit.requestor_username} ${debit.amount} +

+ ))} +
+ ); +} + +export default DebitPage; \ No newline at end of file diff --git a/client/src/pages/FriendshipPage.js b/client/src/pages/FriendshipPage.js new file mode 100644 index 000000000..dd966dd0c --- /dev/null +++ b/client/src/pages/FriendshipPage.js @@ -0,0 +1,123 @@ +import React, { useState, useEffect } from 'react'; + +function FriendshipPage({ currentUser }) { + const [friends, setFriends] = useState([]); + const [friendRequests, setFriendRequests] = useState([]); + const [searchTerm, setSearchTerm] = useState(''); + const [searchResults, setSearchResults] = useState([]); + + useEffect(() => { + fetchFriends(); + fetchFriendRequests(); + }, []); + + const fetchFriends = () => { + fetch('/api/friends') + .then(res => res.json()) + .then(data => setFriends(data)); + }; + + const fetchFriendRequests = () => { + fetch('/api/friend-requests') + .then(res => res.json()) + .then(data => setFriendRequests(data)); + }; + + const handleSearch = (e) => { + e.preventDefault(); + fetch(`/api/users/search?q=${searchTerm}`) + .then(res => res.json()) + .then(data => setSearchResults(data)); + }; + + const handleAddFriend = (userId) => { + fetch('/api/friend-requests', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ userId }), + }) + .then(() => { + setSearchResults(prevResults => + prevResults.map(user => + user.id === userId ? { ...user, requestSent: true } : user + ) + ); + }); + }; + + const handleAcceptRequest = (requestId) => { + fetch(`/api/friend-requests/${requestId}/accept`, { method: 'POST' }) + .then(() => { + fetchFriends(); + fetchFriendRequests(); + }); + }; + + const handleRejectRequest = (requestId) => { + fetch(`/api/friend-requests/${requestId}/reject`, { method: 'POST' }) + .then(() => fetchFriendRequests()); + }; + + const handleDeleteFriend = (friendId) => { + fetch(`/api/friends/${friendId}`, { method: 'DELETE' }) + .then(() => fetchFriends()); + }; + + return ( +
+

Friendship Management

+ +
+

Your Friends

+
    + {friends.map(friend => ( +
  • + {friend.username} + +
  • + ))} +
+
+ +
+

Friend Requests

+
    + {friendRequests.map(request => ( +
  • + {request.sender.username} wants to be your friend + + +
  • + ))} +
+
+ +
+

Search Users

+
+ setSearchTerm(e.target.value)} + placeholder="Search users..." + /> + +
+
    + {searchResults.map(user => ( +
  • + {user.username} + {!user.isFriend && !user.requestSent && ( + + )} + {user.requestSent && Friend request sent} + {user.isFriend && Already friends} +
  • + ))} +
+
+
+ ); +} + +export default FriendshipPage; \ No newline at end of file diff --git a/client/src/pages/StatsPage.js b/client/src/pages/StatsPage.js new file mode 100644 index 000000000..a2720a32a --- /dev/null +++ b/client/src/pages/StatsPage.js @@ -0,0 +1,91 @@ +import React, { useState, useEffect } from 'react'; +import { PieChart, Pie, Cell, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts'; + +function StatsPage({ currentUser }) { + const [stats, setStats] = useState(null); + + useEffect(() => { + fetch('/api/stats') + .then(res => res.json()) + .then(data => setStats(data)); + }, []); + + if (!stats) return
Loading stats...
; + + const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8']; + + return ( +
+

Transaction Statistics

+ +
+

General Stats

+

Total transactions: {stats.totalTransactions}

+

Transactions as sender: {stats.transactionsAsSender}

+

Transactions as receiver: {stats.transactionsAsReceiver}

+
+ +
+

Transaction Partners

+
    + {stats.transactionPartners.map(partner => ( +
  • + {partner.username}: {partner.transactionCount} transactions, + Total amount: ${partner.totalAmount.toFixed(2)} + {partner.isFriend ? ' (Friend)' : ''} +
  • + ))} +
+
+ +
+

Most Active Transaction Partner

+

+ {stats.mostActivePartner.username}: {stats.mostActivePartner.transactionCount} transactions, + Total amount: ${stats.mostActivePartner.totalAmount.toFixed(2)} +

+
+ +
+

Transaction Distribution

+ + + {stats.transactionPartners.map((entry, index) => ( + + ))} + + + + +
+ +
+

Yearly Transaction Summary

+ + + + + + + + + +
+
+ ); +} + +export default StatsPage; \ No newline at end of file diff --git a/server/app.py b/server/app.py index d7f4c5b4c..5ac311e1a 100644 --- a/server/app.py +++ b/server/app.py @@ -3,21 +3,221 @@ # Standard library imports # Remote library imports -from flask import request +from flask import request, session from flask_restful import Resource # Local imports from config import app, db, api -# Add your model imports +# Model imports +from models import User, Transaction + +from sqlalchemy import func, and_ # Views go here! + @app.route('/') def index(): return '

Project Server

' +# Signup +@app.post('/signup') +def create_user(): + data = request.json + try: + existing_user = User.query.filter_by(username=data['username']).first() + if existing_user: + return {'error': 'Username already exists'}, 400 + + new_user = User(username=data['username']) + new_user.password = data['password'] + db.session.add(new_user) + db.session.commit() + session['user_id'] = new_user.id # SETTING COOKIE + return new_user.to_dict(), 201 + except Exception as e: + return {'error': str(e)}, 404 + + +# Check session +@app.get('/check_session') +def check_session(): + user_id = session.get('user_id') + if user_id: + user = User.query.where(User.id == user_id).first() + return user.to_dict(), 200 + else: + return {}, 204 + + +# Login/logout +@app.post('/login') +def login(): + data = request.json + user = User.query.where(User.username == data['username']).first() + if user and user.authenticate(data['password']): # returns true or false + session['user_id'] = user.id + return user.to_dict(), 201 + else: + return {'error': 'Invalid username or password'}, 401 + + +@app.delete('/logout') +def logout(): + session.pop('user_id') + return {}, 204 + + +# Who does the user owe? +@app.get('/debits') +def get_debits(): + debits = ( + db.session.query(Transaction, User.username) + .join(User, Transaction.requestor == User.id) + .filter(Transaction.requestee == session['user_id']) + .all() + ) + + result = [ + { + 'id': transaction.id, + 'requestor': transaction.requestor, + 'requestor_username': username, + 'requestee': transaction.requestee, + 'amount': transaction.amount, + 'year': transaction.year + } + for transaction, username in debits + ] + + return result, 200 + + +# Who owes the user? +@app.get('/credits') +def get_credits(): + credits = ( + db.session.query(Transaction, User.username) + .join(User, Transaction.requestee == User.id) + .where(Transaction.requestor == session['user_id']) + .all() + ) + result = [ + { + 'id': transaction.id, + 'requestor': transaction.requestor, + 'requestee': transaction.requestee, + 'requestee_username': username, + 'amount': transaction.amount, + 'year': transaction.year + } + for transaction, username in credits + ] + + return result, 200 + + +@app.get('/users') +def get_users(): + all_users = User.query.all() + return [user.to_dict() for user in all_users] + + +@app.post('/request') +def add_transaction(): + try: + data = request.json + new_transaction = Transaction( + requestor=data['requestor'], + requestee=data['requestee'], + amount=data['amount'], + year=data['year'] + ) + db.session.add(new_transaction) + db.session.commit() + return new_transaction.to_dict(), 201 + except Exception as e: + + return {'error':str(e)}, 404 + +@app.delete('/payment') +def make_payment(): + data = request.json + payment = Transaction.query.where(data['id'] == Transaction.id).first() + db.session.delete(payment) + db.session.commit() + return {}, 204 + +@app.get('/api/stats') +def get_stats(): + user_id = session.get('user_id') + if not user_id: + return {'error': 'Unauthorized'}, 401 + + # 1. Total transactions + total_transactions = Transaction.query.filter( + (Transaction.requestor == user_id) | (Transaction.requestee == user_id) + ).count() + + # 2. Transactions as sender + transactions_as_sender = Transaction.query.filter(Transaction.requestor == user_id).count() + + # 3. Transactions as receiver + transactions_as_receiver = Transaction.query.filter(Transaction.requestee == user_id).count() + + # 4. Transaction partners + transaction_partners = db.session.query( + User.id, + User.username, + func.count(Transaction.id).label('transaction_count'), + func.sum(case( + (Transaction.requestor == user_id, -Transaction.amount), + else_=Transaction.amount + )).label('total_amount') + ).join( + Transaction, + or_(Transaction.requestor == User.id, Transaction.requestee == User.id) + ).filter( + and_(User.id != user_id, or_(Transaction.requestor == user_id, Transaction.requestee == user_id)) + ).group_by(User.id).all() + + # 5. Most active transaction partner + most_active_partner = max(transaction_partners, key=lambda x: x.transaction_count) + + # 6 & 7. Yearly transaction summary + yearly_transactions = db.session.query( + Transaction.year, + func.sum(case((Transaction.requestor == user_id, Transaction.amount), else_=0)).label('credit'), + func.sum(case((Transaction.requestee == user_id, Transaction.amount), else_=0)).label('debit') + ).filter( + or_(Transaction.requestor == user_id, Transaction.requestee == user_id) + ).group_by(Transaction.year).all() + + return { + 'totalTransactions': total_transactions, + 'transactionsAsSender': transactions_as_sender, + 'transactionsAsReceiver': transactions_as_receiver, + 'transactionPartners': [{ + 'userId': partner.id, + 'username': partner.username, + 'transactionCount': partner.transaction_count, + 'totalAmount': float(partner.total_amount) + } for partner in transaction_partners], + 'mostActivePartner': { + 'userId': most_active_partner.id, + 'username': most_active_partner.username, + 'transactionCount': most_active_partner.transaction_count, + 'totalAmount': float(most_active_partner.total_amount) + }, + 'yearlyTransactions': [{ + 'year': year, + 'credit': float(credit), + 'debit': float(debit) + } for year, credit, debit in yearly_transactions] + }, 200 + + if __name__ == '__main__': app.run(port=5555, debug=True) - diff --git a/server/config.py b/server/config.py index b0603b320..0c736fe63 100644 --- a/server/config.py +++ b/server/config.py @@ -1,17 +1,22 @@ # Standard library imports - # Remote library imports + +import os from flask import Flask from flask_cors import CORS from flask_migrate import Migrate from flask_restful import Api from flask_sqlalchemy import SQLAlchemy from sqlalchemy import MetaData +from flask_bcrypt import Bcrypt +from dotenv import load_dotenv -# Local imports + +load_dotenv() # Instantiate app, set attributes app = Flask(__name__) +app.secret_key = os.environ.get('FLASK_SECRET') #need to pull from env file app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.json.compact = False @@ -29,3 +34,5 @@ # Instantiate CORS CORS(app) + +bcrypt = Bcrypt(app) #needs secret diff --git a/server/migrations/README b/server/migrations/README new file mode 100644 index 000000000..0e0484415 --- /dev/null +++ b/server/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/server/migrations/alembic.ini b/server/migrations/alembic.ini new file mode 100644 index 000000000..ec9d45c26 --- /dev/null +++ b/server/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/server/migrations/env.py b/server/migrations/env.py new file mode 100644 index 000000000..4c9709271 --- /dev/null +++ b/server/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/server/migrations/script.py.mako b/server/migrations/script.py.mako new file mode 100644 index 000000000..2c0156303 --- /dev/null +++ b/server/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/server/migrations/versions/495e3e2e2f2b_.py b/server/migrations/versions/495e3e2e2f2b_.py new file mode 100644 index 000000000..3c5aac14e --- /dev/null +++ b/server/migrations/versions/495e3e2e2f2b_.py @@ -0,0 +1,34 @@ +"""empty message + +Revision ID: 495e3e2e2f2b +Revises: 6f78816ab4e2 +Create Date: 2024-08-16 21:53:50.770997 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '495e3e2e2f2b' +down_revision = '6f78816ab4e2' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('transactions', schema=None) as batch_op: + batch_op.drop_column('requestee_username') + batch_op.drop_column('requestor_username') + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('transactions', schema=None) as batch_op: + batch_op.add_column(sa.Column('requestor_username', sa.VARCHAR(), nullable=True)) + batch_op.add_column(sa.Column('requestee_username', sa.VARCHAR(), nullable=True)) + + # ### end Alembic commands ### diff --git a/server/migrations/versions/6d2e3f33e8ea_add_year_to_transaction_model.py b/server/migrations/versions/6d2e3f33e8ea_add_year_to_transaction_model.py new file mode 100644 index 000000000..6a485ddce --- /dev/null +++ b/server/migrations/versions/6d2e3f33e8ea_add_year_to_transaction_model.py @@ -0,0 +1,32 @@ +"""Add year to Transaction model + +Revision ID: 6d2e3f33e8ea +Revises: 495e3e2e2f2b +Create Date: 2024-08-19 11:45:38.348319 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '6d2e3f33e8ea' +down_revision = '495e3e2e2f2b' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('transactions', schema=None) as batch_op: + batch_op.add_column(sa.Column('year', sa.Integer(), nullable=False)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('transactions', schema=None) as batch_op: + batch_op.drop_column('year') + + # ### end Alembic commands ### diff --git a/server/migrations/versions/6f78816ab4e2_.py b/server/migrations/versions/6f78816ab4e2_.py new file mode 100644 index 000000000..4b652c0d7 --- /dev/null +++ b/server/migrations/versions/6f78816ab4e2_.py @@ -0,0 +1,46 @@ +"""empty message + +Revision ID: 6f78816ab4e2 +Revises: +Create Date: 2024-08-16 21:20:40.361253 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '6f78816ab4e2' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('users', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('username', sa.String(), nullable=False), + sa.Column('_hashed_password', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('username') + ) + op.create_table('transactions', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('requestor', sa.Integer(), nullable=False), + sa.Column('requestor_username', sa.String(), nullable=True), + sa.Column('requestee', sa.Integer(), nullable=False), + sa.Column('requestee_username', sa.String(), nullable=True), + sa.Column('amount', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['requestee'], ['users.id'], name=op.f('fk_transactions_requestee_users')), + sa.ForeignKeyConstraint(['requestor'], ['users.id'], name=op.f('fk_transactions_requestor_users')), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('transactions') + op.drop_table('users') + # ### end Alembic commands ### diff --git a/server/models.py b/server/models.py index c34c5001f..7d2b9f2e8 100644 --- a/server/models.py +++ b/server/models.py @@ -1,6 +1,60 @@ from sqlalchemy_serializer import SerializerMixin from sqlalchemy.ext.associationproxy import association_proxy -from config import db +from config import db, bcrypt # Models go here! + +class User(db.Model, SerializerMixin): + __tablename__ = "users" + + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String, nullable=False, unique=True) + _hashed_password = db.Column(db.String) + + @property + def password(self): + raise Exception('You may not view password') + + @password.setter + def password(self, value): + self._hashed_password = bcrypt.generate_password_hash(value).decode('utf-8') + + def authenticate(self, password_to_check): + return bcrypt.check_password_hash(self._hashed_password, password_to_check) + + # Relationships + transactions_sent = db.relationship('Transaction', foreign_keys='Transaction.requestor', back_populates='sender') + + transactions_received = db.relationship('Transaction', foreign_keys='Transaction.requestee', back_populates='receiver') + + # Serialize rules + serialize_rules = ('-transactions_sent.sender', '-transactions_received.receiver', '-_hashed_password') + + def __repr__(self): + return f"" + +class Transaction(db.Model, SerializerMixin): + __tablename__ = "transactions" + + id = db.Column(db.Integer, primary_key=True) + requestor = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) + requestee = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) + amount = db.Column(db.Integer, nullable=False) + year = db.Column(db.Integer, nullable=False) + + # Relationships + sender = db.relationship('User', + foreign_keys=[requestor], + back_populates='transactions_sent') + + receiver = db.relationship('User', + foreign_keys=[requestee], + back_populates='transactions_received') + + # Serialize rules + serialize_rules = ('-sender', '-receiver') + + + def __repr__(self): + return f"" diff --git a/server/seed.py b/server/seed.py index 849d148ad..bdec039f4 100644 --- a/server/seed.py +++ b/server/seed.py @@ -8,10 +8,42 @@ # Local imports from app import app -from models import db +from models import db, Transaction, User if __name__ == '__main__': fake = Faker() with app.app_context(): print("Starting seed...") - # Seed code goes here! + Transaction.query.delete() + User.query.delete() + + # # Create users + # users = [] + # for _ in range(10): + # user = User(username=fake.unique.user_name()) + # user.password = "password" # Set a default password + # users.append(user) + + # db.session.add_all(users) + # db.session.commit() + + # # Create transactions + # transactions = [] + # for _ in range(50): + # sender = rc(users) + # receiver = rc(users) + # while receiver == sender: + # receiver = rc(users) + + # transaction = Transaction( + # requestor=sender.id, + # requestee=receiver.id, + # amount=randint(1, 1000), + # year=randint(2000, 2023) # Random year between 2000 and 2023 + # ) + # transactions.append(transaction) + + # db.session.add_all(transactions) + db.session.commit() + + print("Seed completed!")