diff --git a/backend/Pipfile b/backend/Pipfile index 6f3ea641..813359bc 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -34,6 +34,7 @@ django-labs-accounts = "*" django-debug-toolbar = "*" django-runtime-options = "*" django-storages = "*" +django-phonenumber-field = {extras = ["phonenumberslite"],version = "*"} pillow = "*" boto3 = "*" apns2 = "*" diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index 258e280a..44d78ce4 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "9108abccde1bd9a442df4c3ea2255d665ee4024a1af12e99653d5f1ffc8c9983" + "sha256": "2f4e0edadfb76b77c318857dcf6a0eaa87b5235de6654e6edfa00e64f341fe11" }, "pipfile-spec": 6, "requires": { @@ -66,20 +66,20 @@ }, "boto3": { "hashes": [ - "sha256:2f18d2dac5d9229e8485b556eb58b7b95fca91bbf002f63bf9c39209f513f6e6", - "sha256:71dcd596a82b5341c6941117b9228897bfb1e2b58f73e60e1fdda1b02a847cc8" + "sha256:a61cf96f7e196b1450afdf4856b7ea0e58080752e687b0011157be96934489be", + "sha256:bbe377a288b6b12b526fae3b3d743318c6868626cf67e1e97f104345a5194b1e" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==1.28.58" + "version": "==1.28.73" }, "botocore": { "hashes": [ - "sha256:002f8bdca8efde50ae7267f342bc1d03a71d76024ce3949e4ffdd1151581c53e", - "sha256:83a3ca4d9247fdbde76c654137e6ab648bd976f652ce2354def1715c838af505" + "sha256:5334c22d5a3f4643931896137c57b2496fef005b039d87d8740e7a28eb31519d", + "sha256:6e9caaa7205e0c0505f4868a4053e96eaf3f4b6bce0368a46970a8efeeacb492" ], "markers": "python_version >= '3.7'", - "version": "==1.31.58" + "version": "==1.31.73" }, "celery": { "hashes": [ @@ -158,99 +158,99 @@ }, "charset-normalizer": { "hashes": [ - "sha256:02673e456dc5ab13659f85196c534dc596d4ef260e4d86e856c3b2773ce09843", - "sha256:02af06682e3590ab952599fbadac535ede5d60d78848e555aa58d0c0abbde786", - "sha256:03680bb39035fbcffe828eae9c3f8afc0428c91d38e7d61aa992ef7a59fb120e", - "sha256:0570d21da019941634a531444364f2482e8db0b3425fcd5ac0c36565a64142c8", - "sha256:09c77f964f351a7369cc343911e0df63e762e42bac24cd7d18525961c81754f4", - "sha256:0d3d5b7db9ed8a2b11a774db2bbea7ba1884430a205dbd54a32d61d7c2a190fa", - "sha256:1063da2c85b95f2d1a430f1c33b55c9c17ffaf5e612e10aeaad641c55a9e2b9d", - "sha256:12ebea541c44fdc88ccb794a13fe861cc5e35d64ed689513a5c03d05b53b7c82", - "sha256:153e7b6e724761741e0974fc4dcd406d35ba70b92bfe3fedcb497226c93b9da7", - "sha256:15b26ddf78d57f1d143bdf32e820fd8935d36abe8a25eb9ec0b5a71c82eb3895", - "sha256:1872d01ac8c618a8da634e232f24793883d6e456a66593135aeafe3784b0848d", - "sha256:187d18082694a29005ba2944c882344b6748d5be69e3a89bf3cc9d878e548d5a", - "sha256:1b2919306936ac6efb3aed1fbf81039f7087ddadb3160882a57ee2ff74fd2382", - "sha256:232ac332403e37e4a03d209a3f92ed9071f7d3dbda70e2a5e9cff1c4ba9f0678", - "sha256:23e8565ab7ff33218530bc817922fae827420f143479b753104ab801145b1d5b", - "sha256:24817cb02cbef7cd499f7c9a2735286b4782bd47a5b3516a0e84c50eab44b98e", - "sha256:249c6470a2b60935bafd1d1d13cd613f8cd8388d53461c67397ee6a0f5dce741", - "sha256:24a91a981f185721542a0b7c92e9054b7ab4fea0508a795846bc5b0abf8118d4", - "sha256:2502dd2a736c879c0f0d3e2161e74d9907231e25d35794584b1ca5284e43f596", - "sha256:250c9eb0f4600361dd80d46112213dff2286231d92d3e52af1e5a6083d10cad9", - "sha256:278c296c6f96fa686d74eb449ea1697f3c03dc28b75f873b65b5201806346a69", - "sha256:2935ffc78db9645cb2086c2f8f4cfd23d9b73cc0dc80334bc30aac6f03f68f8c", - "sha256:2f4a0033ce9a76e391542c182f0d48d084855b5fcba5010f707c8e8c34663d77", - "sha256:30a85aed0b864ac88309b7d94be09f6046c834ef60762a8833b660139cfbad13", - "sha256:380c4bde80bce25c6e4f77b19386f5ec9db230df9f2f2ac1e5ad7af2caa70459", - "sha256:3ae38d325b512f63f8da31f826e6cb6c367336f95e418137286ba362925c877e", - "sha256:3b447982ad46348c02cb90d230b75ac34e9886273df3a93eec0539308a6296d7", - "sha256:3debd1150027933210c2fc321527c2299118aa929c2f5a0a80ab6953e3bd1908", - "sha256:4162918ef3098851fcd8a628bf9b6a98d10c380725df9e04caf5ca6dd48c847a", - "sha256:468d2a840567b13a590e67dd276c570f8de00ed767ecc611994c301d0f8c014f", - "sha256:4cc152c5dd831641e995764f9f0b6589519f6f5123258ccaca8c6d34572fefa8", - "sha256:542da1178c1c6af8873e143910e2269add130a299c9106eef2594e15dae5e482", - "sha256:557b21a44ceac6c6b9773bc65aa1b4cc3e248a5ad2f5b914b91579a32e22204d", - "sha256:5707a746c6083a3a74b46b3a631d78d129edab06195a92a8ece755aac25a3f3d", - "sha256:588245972aca710b5b68802c8cad9edaa98589b1b42ad2b53accd6910dad3545", - "sha256:5adf257bd58c1b8632046bbe43ee38c04e1038e9d37de9c57a94d6bd6ce5da34", - "sha256:619d1c96099be5823db34fe89e2582b336b5b074a7f47f819d6b3a57ff7bdb86", - "sha256:63563193aec44bce707e0c5ca64ff69fa72ed7cf34ce6e11d5127555756fd2f6", - "sha256:67b8cc9574bb518ec76dc8e705d4c39ae78bb96237cb533edac149352c1f39fe", - "sha256:6a685067d05e46641d5d1623d7c7fdf15a357546cbb2f71b0ebde91b175ffc3e", - "sha256:70f1d09c0d7748b73290b29219e854b3207aea922f839437870d8cc2168e31cc", - "sha256:750b446b2ffce1739e8578576092179160f6d26bd5e23eb1789c4d64d5af7dc7", - "sha256:7966951325782121e67c81299a031f4c115615e68046f79b85856b86ebffc4cd", - "sha256:7b8b8bf1189b3ba9b8de5c8db4d541b406611a71a955bbbd7385bbc45fcb786c", - "sha256:7f5d10bae5d78e4551b7be7a9b29643a95aded9d0f602aa2ba584f0388e7a557", - "sha256:805dfea4ca10411a5296bcc75638017215a93ffb584c9e344731eef0dcfb026a", - "sha256:81bf654678e575403736b85ba3a7867e31c2c30a69bc57fe88e3ace52fb17b89", - "sha256:82eb849f085624f6a607538ee7b83a6d8126df6d2f7d3b319cb837b289123078", - "sha256:85a32721ddde63c9df9ebb0d2045b9691d9750cb139c161c80e500d210f5e26e", - "sha256:86d1f65ac145e2c9ed71d8ffb1905e9bba3a91ae29ba55b4c46ae6fc31d7c0d4", - "sha256:86f63face3a527284f7bb8a9d4f78988e3c06823f7bea2bd6f0e0e9298ca0403", - "sha256:8eaf82f0eccd1505cf39a45a6bd0a8cf1c70dcfc30dba338207a969d91b965c0", - "sha256:93aa7eef6ee71c629b51ef873991d6911b906d7312c6e8e99790c0f33c576f89", - "sha256:96c2b49eb6a72c0e4991d62406e365d87067ca14c1a729a870d22354e6f68115", - "sha256:9cf3126b85822c4e53aa28c7ec9869b924d6fcfb76e77a45c44b83d91afd74f9", - "sha256:9fe359b2e3a7729010060fbca442ca225280c16e923b37db0e955ac2a2b72a05", - "sha256:a0ac5e7015a5920cfce654c06618ec40c33e12801711da6b4258af59a8eff00a", - "sha256:a3f93dab657839dfa61025056606600a11d0b696d79386f974e459a3fbc568ec", - "sha256:a4b71f4d1765639372a3b32d2638197f5cd5221b19531f9245fcc9ee62d38f56", - "sha256:aae32c93e0f64469f74ccc730a7cb21c7610af3a775157e50bbd38f816536b38", - "sha256:aaf7b34c5bc56b38c931a54f7952f1ff0ae77a2e82496583b247f7c969eb1479", - "sha256:abecce40dfebbfa6abf8e324e1860092eeca6f7375c8c4e655a8afb61af58f2c", - "sha256:abf0d9f45ea5fb95051c8bfe43cb40cda383772f7e5023a83cc481ca2604d74e", - "sha256:ac71b2977fb90c35d41c9453116e283fac47bb9096ad917b8819ca8b943abecd", - "sha256:ada214c6fa40f8d800e575de6b91a40d0548139e5dc457d2ebb61470abf50186", - "sha256:b09719a17a2301178fac4470d54b1680b18a5048b481cb8890e1ef820cb80455", - "sha256:b1121de0e9d6e6ca08289583d7491e7fcb18a439305b34a30b20d8215922d43c", - "sha256:b3b2316b25644b23b54a6f6401074cebcecd1244c0b8e80111c9a3f1c8e83d65", - "sha256:b3d9b48ee6e3967b7901c052b670c7dda6deb812c309439adaffdec55c6d7b78", - "sha256:b5bcf60a228acae568e9911f410f9d9e0d43197d030ae5799e20dca8df588287", - "sha256:b8f3307af845803fb0b060ab76cf6dd3a13adc15b6b451f54281d25911eb92df", - "sha256:c2af80fb58f0f24b3f3adcb9148e6203fa67dd3f61c4af146ecad033024dde43", - "sha256:c350354efb159b8767a6244c166f66e67506e06c8924ed74669b2c70bc8735b1", - "sha256:c5a74c359b2d47d26cdbbc7845e9662d6b08a1e915eb015d044729e92e7050b7", - "sha256:c71f16da1ed8949774ef79f4a0260d28b83b3a50c6576f8f4f0288d109777989", - "sha256:d47ecf253780c90ee181d4d871cd655a789da937454045b17b5798da9393901a", - "sha256:d7eff0f27edc5afa9e405f7165f85a6d782d308f3b6b9d96016c010597958e63", - "sha256:d97d85fa63f315a8bdaba2af9a6a686e0eceab77b3089af45133252618e70884", - "sha256:db756e48f9c5c607b5e33dd36b1d5872d0422e960145b08ab0ec7fd420e9d649", - "sha256:dc45229747b67ffc441b3de2f3ae5e62877a282ea828a5bdb67883c4ee4a8810", - "sha256:e0fc42822278451bc13a2e8626cf2218ba570f27856b536e00cfa53099724828", - "sha256:e39c7eb31e3f5b1f88caff88bcff1b7f8334975b46f6ac6e9fc725d829bc35d4", - "sha256:e46cd37076971c1040fc8c41273a8b3e2c624ce4f2be3f5dfcb7a430c1d3acc2", - "sha256:e5c1502d4ace69a179305abb3f0bb6141cbe4714bc9b31d427329a95acfc8bdd", - "sha256:edfe077ab09442d4ef3c52cb1f9dab89bff02f4524afc0acf2d46be17dc479f5", - "sha256:effe5406c9bd748a871dbcaf3ac69167c38d72db8c9baf3ff954c344f31c4cbe", - "sha256:f0d1e3732768fecb052d90d62b220af62ead5748ac51ef61e7b32c266cac9293", - "sha256:f5969baeaea61c97efa706b9b107dcba02784b1601c74ac84f2a532ea079403e", - "sha256:f8888e31e3a85943743f8fc15e71536bda1c81d5aa36d014a3c0c44481d7db6e", - "sha256:fc52b79d83a3fe3a360902d3f5d79073a993597d48114c29485e9431092905d8" + "sha256:06cf46bdff72f58645434d467bf5228080801298fbba19fe268a01b4534467f5", + "sha256:0c8c61fb505c7dad1d251c284e712d4e0372cef3b067f7ddf82a7fa82e1e9a93", + "sha256:10b8dd31e10f32410751b3430996f9807fc4d1587ca69772e2aa940a82ab571a", + "sha256:1171ef1fc5ab4693c5d151ae0fdad7f7349920eabbaca6271f95969fa0756c2d", + "sha256:17a866d61259c7de1bdadef418a37755050ddb4b922df8b356503234fff7932c", + "sha256:1d6bfc32a68bc0933819cfdfe45f9abc3cae3877e1d90aac7259d57e6e0f85b1", + "sha256:1ec937546cad86d0dce5396748bf392bb7b62a9eeb8c66efac60e947697f0e58", + "sha256:223b4d54561c01048f657fa6ce41461d5ad8ff128b9678cfe8b2ecd951e3f8a2", + "sha256:2465aa50c9299d615d757c1c888bc6fef384b7c4aec81c05a0172b4400f98557", + "sha256:28f512b9a33235545fbbdac6a330a510b63be278a50071a336afc1b78781b147", + "sha256:2c092be3885a1b7899cd85ce24acedc1034199d6fca1483fa2c3a35c86e43041", + "sha256:2c4c99f98fc3a1835af8179dcc9013f93594d0670e2fa80c83aa36346ee763d2", + "sha256:31445f38053476a0c4e6d12b047b08ced81e2c7c712e5a1ad97bc913256f91b2", + "sha256:31bbaba7218904d2eabecf4feec0d07469284e952a27400f23b6628439439fa7", + "sha256:34d95638ff3613849f473afc33f65c401a89f3b9528d0d213c7037c398a51296", + "sha256:352a88c3df0d1fa886562384b86f9a9e27563d4704ee0e9d56ec6fcd270ea690", + "sha256:39b70a6f88eebe239fa775190796d55a33cfb6d36b9ffdd37843f7c4c1b5dc67", + "sha256:3c66df3f41abee950d6638adc7eac4730a306b022570f71dd0bd6ba53503ab57", + "sha256:3f70fd716855cd3b855316b226a1ac8bdb3caf4f7ea96edcccc6f484217c9597", + "sha256:3f9bc2ce123637a60ebe819f9fccc614da1bcc05798bbbaf2dd4ec91f3e08846", + "sha256:3fb765362688821404ad6cf86772fc54993ec11577cd5a92ac44b4c2ba52155b", + "sha256:45f053a0ece92c734d874861ffe6e3cc92150e32136dd59ab1fb070575189c97", + "sha256:46fb9970aa5eeca547d7aa0de5d4b124a288b42eaefac677bde805013c95725c", + "sha256:4cb50a0335382aac15c31b61d8531bc9bb657cfd848b1d7158009472189f3d62", + "sha256:4e12f8ee80aa35e746230a2af83e81bd6b52daa92a8afaef4fea4a2ce9b9f4fa", + "sha256:4f3100d86dcd03c03f7e9c3fdb23d92e32abbca07e7c13ebd7ddfbcb06f5991f", + "sha256:4f6e2a839f83a6a76854d12dbebde50e4b1afa63e27761549d006fa53e9aa80e", + "sha256:4f861d94c2a450b974b86093c6c027888627b8082f1299dfd5a4bae8e2292821", + "sha256:501adc5eb6cd5f40a6f77fbd90e5ab915c8fd6e8c614af2db5561e16c600d6f3", + "sha256:520b7a142d2524f999447b3a0cf95115df81c4f33003c51a6ab637cbda9d0bf4", + "sha256:548eefad783ed787b38cb6f9a574bd8664468cc76d1538215d510a3cd41406cb", + "sha256:555fe186da0068d3354cdf4bbcbc609b0ecae4d04c921cc13e209eece7720727", + "sha256:55602981b2dbf8184c098bc10287e8c245e351cd4fdcad050bd7199d5a8bf514", + "sha256:58e875eb7016fd014c0eea46c6fa92b87b62c0cb31b9feae25cbbe62c919f54d", + "sha256:5a3580a4fdc4ac05f9e53c57f965e3594b2f99796231380adb2baaab96e22761", + "sha256:5b70bab78accbc672f50e878a5b73ca692f45f5b5e25c8066d748c09405e6a55", + "sha256:5ceca5876032362ae73b83347be8b5dbd2d1faf3358deb38c9c88776779b2e2f", + "sha256:61f1e3fb621f5420523abb71f5771a204b33c21d31e7d9d86881b2cffe92c47c", + "sha256:633968254f8d421e70f91c6ebe71ed0ab140220469cf87a9857e21c16687c034", + "sha256:63a6f59e2d01310f754c270e4a257426fe5a591dc487f1983b3bbe793cf6bac6", + "sha256:63accd11149c0f9a99e3bc095bbdb5a464862d77a7e309ad5938fbc8721235ae", + "sha256:6db3cfb9b4fcecb4390db154e75b49578c87a3b9979b40cdf90d7e4b945656e1", + "sha256:71ef3b9be10070360f289aea4838c784f8b851be3ba58cf796262b57775c2f14", + "sha256:7ae8e5142dcc7a49168f4055255dbcced01dc1714a90a21f87448dc8d90617d1", + "sha256:7b6cefa579e1237ce198619b76eaa148b71894fb0d6bcf9024460f9bf30fd228", + "sha256:800561453acdecedaac137bf09cd719c7a440b6800ec182f077bb8e7025fb708", + "sha256:82ca51ff0fc5b641a2d4e1cc8c5ff108699b7a56d7f3ad6f6da9dbb6f0145b48", + "sha256:851cf693fb3aaef71031237cd68699dded198657ec1e76a76eb8be58c03a5d1f", + "sha256:854cc74367180beb327ab9d00f964f6d91da06450b0855cbbb09187bcdb02de5", + "sha256:87071618d3d8ec8b186d53cb6e66955ef2a0e4fa63ccd3709c0c90ac5a43520f", + "sha256:871d045d6ccc181fd863a3cd66ee8e395523ebfbc57f85f91f035f50cee8e3d4", + "sha256:8aee051c89e13565c6bd366813c386939f8e928af93c29fda4af86d25b73d8f8", + "sha256:8af5a8917b8af42295e86b64903156b4f110a30dca5f3b5aedea123fbd638bff", + "sha256:8ec8ef42c6cd5856a7613dcd1eaf21e5573b2185263d87d27c8edcae33b62a61", + "sha256:91e43805ccafa0a91831f9cd5443aa34528c0c3f2cc48c4cb3d9a7721053874b", + "sha256:9505dc359edb6a330efcd2be825fdb73ee3e628d9010597aa1aee5aa63442e97", + "sha256:985c7965f62f6f32bf432e2681173db41336a9c2611693247069288bcb0c7f8b", + "sha256:9a74041ba0bfa9bc9b9bb2cd3238a6ab3b7618e759b41bd15b5f6ad958d17605", + "sha256:9edbe6a5bf8b56a4a84533ba2b2f489d0046e755c29616ef8830f9e7d9cf5728", + "sha256:a15c1fe6d26e83fd2e5972425a772cca158eae58b05d4a25a4e474c221053e2d", + "sha256:a66bcdf19c1a523e41b8e9d53d0cedbfbac2e93c649a2e9502cb26c014d0980c", + "sha256:ae4070f741f8d809075ef697877fd350ecf0b7c5837ed68738607ee0a2c572cf", + "sha256:ae55d592b02c4349525b6ed8f74c692509e5adffa842e582c0f861751701a673", + "sha256:b578cbe580e3b41ad17b1c428f382c814b32a6ce90f2d8e39e2e635d49e498d1", + "sha256:b891a2f68e09c5ef989007fac11476ed33c5c9994449a4e2c3386529d703dc8b", + "sha256:baec8148d6b8bd5cee1ae138ba658c71f5b03e0d69d5907703e3e1df96db5e41", + "sha256:bb06098d019766ca16fc915ecaa455c1f1cd594204e7f840cd6258237b5079a8", + "sha256:bc791ec3fd0c4309a753f95bb6c749ef0d8ea3aea91f07ee1cf06b7b02118f2f", + "sha256:bd28b31730f0e982ace8663d108e01199098432a30a4c410d06fe08fdb9e93f4", + "sha256:be4d9c2770044a59715eb57c1144dedea7c5d5ae80c68fb9959515037cde2008", + "sha256:c0c72d34e7de5604df0fde3644cc079feee5e55464967d10b24b1de268deceb9", + "sha256:c0e842112fe3f1a4ffcf64b06dc4c61a88441c2f02f373367f7b4c1aa9be2ad5", + "sha256:c15070ebf11b8b7fd1bfff7217e9324963c82dbdf6182ff7050519e350e7ad9f", + "sha256:c2000c54c395d9e5e44c99dc7c20a64dc371f777faf8bae4919ad3e99ce5253e", + "sha256:c30187840d36d0ba2893bc3271a36a517a717f9fd383a98e2697ee890a37c273", + "sha256:cb7cd68814308aade9d0c93c5bd2ade9f9441666f8ba5aa9c2d4b389cb5e2a45", + "sha256:cd805513198304026bd379d1d516afbf6c3c13f4382134a2c526b8b854da1c2e", + "sha256:d0bf89afcbcf4d1bb2652f6580e5e55a840fdf87384f6063c4a4f0c95e378656", + "sha256:d9137a876020661972ca6eec0766d81aef8a5627df628b664b234b73396e727e", + "sha256:dbd95e300367aa0827496fe75a1766d198d34385a58f97683fe6e07f89ca3e3c", + "sha256:dced27917823df984fe0c80a5c4ad75cf58df0fbfae890bc08004cd3888922a2", + "sha256:de0b4caa1c8a21394e8ce971997614a17648f94e1cd0640fbd6b4d14cab13a72", + "sha256:debb633f3f7856f95ad957d9b9c781f8e2c6303ef21724ec94bea2ce2fcbd056", + "sha256:e372d7dfd154009142631de2d316adad3cc1c36c32a38b16a4751ba78da2a397", + "sha256:ecd26be9f112c4f96718290c10f4caea6cc798459a3a76636b817a0ed7874e42", + "sha256:edc0202099ea1d82844316604e17d2b175044f9bcb6b398aab781eba957224bd", + "sha256:f194cce575e59ffe442c10a360182a986535fd90b57f7debfaa5c845c409ecc3", + "sha256:f5fb672c396d826ca16a022ac04c9dce74e00a1c344f6ad1a0fdc1ba1f332213", + "sha256:f6a02a3c7950cafaadcd46a226ad9e12fc9744652cc69f9e5534f98b47f3bbcf", + "sha256:fe81b35c33772e56f4b6cf62cf4aedc1762ef7162a31e6ac7fe5e40d0149eb67" ], "markers": "python_full_version >= '3.7.0'", - "version": "==3.3.0" + "version": "==3.3.1" }, "click": { "hashes": [ @@ -286,32 +286,32 @@ }, "cryptography": { "hashes": [ - "sha256:004b6ccc95943f6a9ad3142cfabcc769d7ee38a3f60fb0dddbfb431f818c3a67", - "sha256:047c4603aeb4bbd8db2756e38f5b8bd7e94318c047cfe4efeb5d715e08b49311", - "sha256:0d9409894f495d465fe6fda92cb70e8323e9648af912d5b9141d616df40a87b8", - "sha256:23a25c09dfd0d9f28da2352503b23e086f8e78096b9fd585d1d14eca01613e13", - "sha256:2ed09183922d66c4ec5fdaa59b4d14e105c084dd0febd27452de8f6f74704143", - "sha256:35c00f637cd0b9d5b6c6bd11b6c3359194a8eba9c46d4e875a3660e3b400005f", - "sha256:37480760ae08065437e6573d14be973112c9e6dcaf5f11d00147ee74f37a3829", - "sha256:3b224890962a2d7b57cf5eeb16ccaafba6083f7b811829f00476309bce2fe0fd", - "sha256:5a0f09cefded00e648a127048119f77bc2b2ec61e736660b5789e638f43cc397", - "sha256:5b72205a360f3b6176485a333256b9bcd48700fc755fef51c8e7e67c4b63e3ac", - "sha256:7e53db173370dea832190870e975a1e09c86a879b613948f09eb49324218c14d", - "sha256:7febc3094125fc126a7f6fb1f420d0da639f3f32cb15c8ff0dc3997c4549f51a", - "sha256:80907d3faa55dc5434a16579952ac6da800935cd98d14dbd62f6f042c7f5e839", - "sha256:86defa8d248c3fa029da68ce61fe735432b047e32179883bdb1e79ed9bb8195e", - "sha256:8ac4f9ead4bbd0bc8ab2d318f97d85147167a488be0e08814a37eb2f439d5cf6", - "sha256:93530900d14c37a46ce3d6c9e6fd35dbe5f5601bf6b3a5c325c7bffc030344d9", - "sha256:9eeb77214afae972a00dee47382d2591abe77bdae166bda672fb1e24702a3860", - "sha256:b5f4dfe950ff0479f1f00eda09c18798d4f49b98f4e2006d644b3301682ebdca", - "sha256:c3391bd8e6de35f6f1140e50aaeb3e2b3d6a9012536ca23ab0d9c35ec18c8a91", - "sha256:c880eba5175f4307129784eca96f4e70b88e57aa3f680aeba3bab0e980b0f37d", - "sha256:cecfefa17042941f94ab54f769c8ce0fe14beff2694e9ac684176a2535bf9714", - "sha256:e40211b4923ba5a6dc9769eab704bdb3fbb58d56c5b336d30996c24fcf12aadb", - "sha256:efc8ad4e6fc4f1752ebfb58aefece8b4e3c4cae940b0994d43649bdfce8d0d4f" + "sha256:0c327cac00f082013c7c9fb6c46b7cc9fa3c288ca702c74773968173bda421bf", + "sha256:0d2a6a598847c46e3e321a7aef8af1436f11c27f1254933746304ff014664d84", + "sha256:227ec057cd32a41c6651701abc0328135e472ed450f47c2766f23267b792a88e", + "sha256:22892cc830d8b2c89ea60148227631bb96a7da0c1b722f2aac8824b1b7c0b6b8", + "sha256:392cb88b597247177172e02da6b7a63deeff1937fa6fec3bbf902ebd75d97ec7", + "sha256:3be3ca726e1572517d2bef99a818378bbcf7d7799d5372a46c79c29eb8d166c1", + "sha256:573eb7128cbca75f9157dcde974781209463ce56b5804983e11a1c462f0f4e88", + "sha256:580afc7b7216deeb87a098ef0674d6ee34ab55993140838b14c9b83312b37b86", + "sha256:5a70187954ba7292c7876734183e810b728b4f3965fbe571421cb2434d279179", + "sha256:73801ac9736741f220e20435f84ecec75ed70eda90f781a148f1bad546963d81", + "sha256:7d208c21e47940369accfc9e85f0de7693d9a5d843c2509b3846b2db170dfd20", + "sha256:8254962e6ba1f4d2090c44daf50a547cd5f0bf446dc658a8e5f8156cae0d8548", + "sha256:88417bff20162f635f24f849ab182b092697922088b477a7abd6664ddd82291d", + "sha256:a48e74dad1fb349f3dc1d449ed88e0017d792997a7ad2ec9587ed17405667e6d", + "sha256:b948e09fe5fb18517d99994184854ebd50b57248736fd4c720ad540560174ec5", + "sha256:c707f7afd813478e2019ae32a7c49cd932dd60ab2d2a93e796f68236b7e1fbf1", + "sha256:d38e6031e113b7421db1de0c1b1f7739564a88f1684c6b89234fbf6c11b75147", + "sha256:d3977f0e276f6f5bf245c403156673db103283266601405376f075c849a0b936", + "sha256:da6a0ff8f1016ccc7477e6339e1d50ce5f59b88905585f77193ebd5068f1e797", + "sha256:e270c04f4d9b5671ebcc792b3ba5d4488bf7c42c3c241a3748e2599776f29696", + "sha256:e886098619d3815e0ad5790c973afeee2c0e6e04b4da90b88e6bd06e2a0b1b72", + "sha256:ec3b055ff8f1dce8e6ef28f626e0972981475173d7973d63f271b29c8a2897da", + "sha256:fba1e91467c65fe64a82c689dc6cf58151158993b13eb7a7f3f4b7f395636723" ], "markers": "python_version >= '3.7'", - "version": "==41.0.4" + "version": "==41.0.5" }, "deprecated": { "hashes": [ @@ -331,21 +331,21 @@ }, "django": { "hashes": [ - "sha256:a5de4c484e7b7418e6d3e52a5b8794f0e6b9f9e4ce3c037018cf1c489fa87f3c", - "sha256:d31b06c58aa2cd73998ca5966bc3001243d3c4e77ee2d0c479bced124765fd99" + "sha256:83b6d66b06e484807d778263fdc7f9186d4dc1862fcfa6507830446ac6b060ba", + "sha256:c5e7b668025a6e06cad9ba6d4de1fd1a21212acebb51ea34abb400c6e4d33430" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==3.2.21" + "version": "==3.2.22" }, "django-cors-headers": { "hashes": [ - "sha256:9ada212b0e2efd4a5e339360ffc869cb21ac5605e810afe69f7308e577ea5bde", - "sha256:f9749c6410fe738278bc2b6ef17f05195bc7b251693c035752d8257026af024f" + "sha256:25aabc94d4837678c1edf442c7f68a5f5fd151f6767b0e0b01c61a2179d02711", + "sha256:bd36c7aea0d070e462f3383f0dc9ef717e5fdc2b10a99c98c285f16da84ffba2" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.2.0" + "version": "==4.3.0" }, "django-debug-toolbar": { "hashes": [ @@ -374,6 +374,17 @@ "markers": "python_version >= '3.8' and python_version < '4.0'", "version": "==0.9.4" }, + "django-phonenumber-field": { + "extras": [ + "phonenumberslite" + ], + "hashes": [ + "sha256:16778f2717ea2aecc6178beb0d6bc431c78c6a8b0474e1fa8face040efeb6e9e", + "sha256:20c7c5c449e33eed5fd45ef8d3dc668faabaeff3277eddd1892b262d686ba381" + ], + "markers": "python_version >= '3.8'", + "version": "==7.2.0" + }, "django-redis": { "hashes": [ "sha256:6a02abaa34b0fea8bf9b707d2c363ab6adc7409950b2db93602e6cb292818c42", @@ -394,12 +405,12 @@ }, "django-storages": { "hashes": [ - "sha256:18cb6c305fbb2f114c11b5b7b647b6271aa251972dcd4a5651b9cee2b0bd3a8a", - "sha256:a2c327d67792eec04c7f5f5bb2900b21f426de8a3a811cea85fac7904bdccf36" + "sha256:1db759346b52ada6c2efd9f23d8241ecf518813eb31db9e2589207174f58f6ad", + "sha256:51b36af28cc5813b98d5f3dfe7459af638d84428c8df4a03990c7d74d1bea4e5" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==1.14.1" + "version": "==1.14.2" }, "djangorestframework": { "hashes": [ @@ -480,42 +491,42 @@ }, "numpy": { "hashes": [ - "sha256:020cdbee66ed46b671429c7265cf00d8ac91c046901c55684954c3958525dab2", - "sha256:0621f7daf973d34d18b4e4bafb210bbaf1ef5e0100b5fa750bd9cde84c7ac292", - "sha256:0792824ce2f7ea0c82ed2e4fecc29bb86bee0567a080dacaf2e0a01fe7654369", - "sha256:09aaee96c2cbdea95de76ecb8a586cb687d281c881f5f17bfc0fb7f5890f6b91", - "sha256:166b36197e9debc4e384e9c652ba60c0bacc216d0fc89e78f973a9760b503388", - "sha256:186ba67fad3c60dbe8a3abff3b67a91351100f2661c8e2a80364ae6279720299", - "sha256:306545e234503a24fe9ae95ebf84d25cba1fdc27db971aa2d9f1ab6bba19a9dd", - "sha256:436c8e9a4bdeeee84e3e59614d38c3dbd3235838a877af8c211cfcac8a80b8d3", - "sha256:4a873a8180479bc829313e8d9798d5234dfacfc2e8a7ac188418189bb8eafbd2", - "sha256:4acc65dd65da28060e206c8f27a573455ed724e6179941edb19f97e58161bb69", - "sha256:51be5f8c349fdd1a5568e72713a21f518e7d6707bcf8503b528b88d33b57dc68", - "sha256:546b7dd7e22f3c6861463bebb000646fa730e55df5ee4a0224408b5694cc6148", - "sha256:5671338034b820c8d58c81ad1dafc0ed5a00771a82fccc71d6438df00302094b", - "sha256:637c58b468a69869258b8ae26f4a4c6ff8abffd4a8334c830ffb63e0feefe99a", - "sha256:767254ad364991ccfc4d81b8152912e53e103ec192d1bb4ea6b1f5a7117040be", - "sha256:7d484292eaeb3e84a51432a94f53578689ffdea3f90e10c8b203a99be5af57d8", - "sha256:7f6bad22a791226d0a5c7c27a80a20e11cfe09ad5ef9084d4d3fc4a299cca505", - "sha256:86f737708b366c36b76e953c46ba5827d8c27b7a8c9d0f471810728e5a2fe57c", - "sha256:8c6adc33561bd1d46f81131d5352348350fc23df4d742bb246cdfca606ea1208", - "sha256:914b28d3215e0c721dc75db3ad6d62f51f630cb0c277e6b3bcb39519bed10bd8", - "sha256:b44e6a09afc12952a7d2a58ca0a2429ee0d49a4f89d83a0a11052da696440e49", - "sha256:bb0d9a1aaf5f1cb7967320e80690a1d7ff69f1d47ebc5a9bea013e3a21faec95", - "sha256:c0b45c8b65b79337dee5134d038346d30e109e9e2e9d43464a2970e5c0e93229", - "sha256:c2e698cb0c6dda9372ea98a0344245ee65bdc1c9dd939cceed6bb91256837896", - "sha256:c78a22e95182fb2e7874712433eaa610478a3caf86f28c621708d35fa4fd6e7f", - "sha256:e062aa24638bb5018b7841977c360d2f5917268d125c833a686b7cbabbec496c", - "sha256:e5e18e5b14a7560d8acf1c596688f4dfd19b4f2945b245a71e5af4ddb7422feb", - "sha256:eae430ecf5794cb7ae7fa3808740b015aa80747e5266153128ef055975a72b99", - "sha256:ee84ca3c58fe48b8ddafdeb1db87388dce2c3c3f701bf447b05e4cfcc3679112", - "sha256:f042f66d0b4ae6d48e70e28d487376204d3cbf43b84c03bac57e28dac6151581", - "sha256:f8db2f125746e44dce707dd44d4f4efeea8d7e2b43aace3f8d1f235cfa2733dd", - "sha256:f93fc78fe8bf15afe2b8d6b6499f1c73953169fad1e9a8dd086cdff3190e7fdf" + "sha256:06934e1a22c54636a059215d6da99e23286424f316fddd979f5071093b648668", + "sha256:1c59c046c31a43310ad0199d6299e59f57a289e22f0f36951ced1c9eac3665b9", + "sha256:1d1bd82d539607951cac963388534da3b7ea0e18b149a53cf883d8f699178c0f", + "sha256:1e11668d6f756ca5ef534b5be8653d16c5352cbb210a5c2a79ff288e937010d5", + "sha256:3649d566e2fc067597125428db15d60eb42a4e0897fc48d28cb75dc2e0454e53", + "sha256:59227c981d43425ca5e5c01094d59eb14e8772ce6975d4b2fc1e106a833d5ae2", + "sha256:6081aed64714a18c72b168a9276095ef9155dd7888b9e74b5987808f0dd0a974", + "sha256:6965888d65d2848e8768824ca8288db0a81263c1efccec881cb35a0d805fcd2f", + "sha256:76ff661a867d9272cd2a99eed002470f46dbe0943a5ffd140f49be84f68ffc42", + "sha256:78ca54b2f9daffa5f323f34cdf21e1d9779a54073f0018a3094ab907938331a2", + "sha256:82e871307a6331b5f09efda3c22e03c095d957f04bf6bc1804f30048d0e5e7af", + "sha256:8ab9163ca8aeb7fd32fe93866490654d2f7dda4e61bc6297bf72ce07fdc02f67", + "sha256:9696aa2e35cc41e398a6d42d147cf326f8f9d81befcb399bc1ed7ffea339b64e", + "sha256:97e5d6a9f0702c2863aaabf19f0d1b6c2628fbe476438ce0b5ce06e83085064c", + "sha256:9f42284ebf91bdf32fafac29d29d4c07e5e9d1af862ea73686581773ef9e73a7", + "sha256:a03fb25610ef560a6201ff06df4f8105292ba56e7cdd196ea350d123fc32e24e", + "sha256:a5b411040beead47a228bde3b2241100454a6abde9df139ed087bd73fc0a4908", + "sha256:af22f3d8e228d84d1c0c44c1fbdeb80f97a15a0abe4f080960393a00db733b66", + "sha256:afd5ced4e5a96dac6725daeb5242a35494243f2239244fad10a90ce58b071d24", + "sha256:b9d45d1dbb9de84894cc50efece5b09939752a2d75aab3a8b0cef6f3a35ecd6b", + "sha256:bb894accfd16b867d8643fc2ba6c8617c78ba2828051e9a69511644ce86ce83e", + "sha256:c8c6c72d4a9f831f328efb1312642a1cafafaa88981d9ab76368d50d07d93cbe", + "sha256:cd7837b2b734ca72959a1caf3309457a318c934abef7a43a14bb984e574bbb9a", + "sha256:cdd9ec98f0063d93baeb01aad472a1a0840dee302842a2746a7a8e92968f9575", + "sha256:d1cfc92db6af1fd37a7bb58e55c8383b4aa1ba23d012bdbba26b4bcca45ac297", + "sha256:d1d2c6b7dd618c41e202c59c1413ef9b2c8e8a15f5039e344af64195459e3104", + "sha256:d2984cb6caaf05294b8466966627e80bf6c7afd273279077679cb010acb0e5ab", + "sha256:d58e8c51a7cf43090d124d5073bc29ab2755822181fcad978b12e144e5e5a4b3", + "sha256:d78f269e0c4fd365fc2992c00353e4530d274ba68f15e968d8bc3c69ce5f5244", + "sha256:dcfaf015b79d1f9f9c9fd0731a907407dc3e45769262d657d754c3a028586124", + "sha256:e44ccb93f30c75dfc0c3aa3ce38f33486a75ec9abadabd4e59f114994a9c4617", + "sha256:e509cbc488c735b43b5ffea175235cec24bbc57b227ef1acc691725beb230d1c" ], "index": "pypi", "markers": "python_version < '3.13' and python_version >= '3.9'", - "version": "==1.26.0" + "version": "==1.26.1" }, "oauthlib": { "hashes": [ @@ -527,96 +538,103 @@ }, "pandas": { "hashes": [ - "sha256:02304e11582c5d090e5a52aec726f31fe3f42895d6bfc1f28738f9b64b6f0614", - "sha256:0489b0e6aa3d907e909aef92975edae89b1ee1654db5eafb9be633b0124abe97", - "sha256:05674536bd477af36aa2effd4ec8f71b92234ce0cc174de34fd21e2ee99adbc2", - "sha256:25e8474a8eb258e391e30c288eecec565bfed3e026f312b0cbd709a63906b6f8", - "sha256:29deb61de5a8a93bdd033df328441a79fcf8dd3c12d5ed0b41a395eef9cd76f0", - "sha256:366da7b0e540d1b908886d4feb3d951f2f1e572e655c1160f5fde28ad4abb750", - "sha256:3bcad1e6fb34b727b016775bea407311f7721db87e5b409e6542f4546a4951ea", - "sha256:4c3f32fd7c4dccd035f71734df39231ac1a6ff95e8bdab8d891167197b7018d2", - "sha256:4cdb0fab0400c2cb46dafcf1a0fe084c8bb2480a1fa8d81e19d15e12e6d4ded2", - "sha256:4f99bebf19b7e03cf80a4e770a3e65eee9dd4e2679039f542d7c1ace7b7b1daa", - "sha256:58d997dbee0d4b64f3cb881a24f918b5f25dd64ddf31f467bb9b67ae4c63a1e4", - "sha256:75ce97667d06d69396d72be074f0556698c7f662029322027c226fd7a26965cb", - "sha256:84e7e910096416adec68075dc87b986ff202920fb8704e6d9c8c9897fe7332d6", - "sha256:9e2959720b70e106bb1d8b6eadd8ecd7c8e99ccdbe03ee03260877184bb2877d", - "sha256:9e50e72b667415a816ac27dfcfe686dc5a0b02202e06196b943d54c4f9c7693e", - "sha256:a0dbfea0dd3901ad4ce2306575c54348d98499c95be01b8d885a2737fe4d7a98", - "sha256:b407381258a667df49d58a1b637be33e514b07f9285feb27769cedb3ab3d0b3a", - "sha256:b8bd1685556f3374520466998929bade3076aeae77c3e67ada5ed2b90b4de7f0", - "sha256:c1f84c144dee086fe4f04a472b5cd51e680f061adf75c1ae4fc3a9275560f8f4", - "sha256:c747793c4e9dcece7bb20156179529898abf505fe32cb40c4052107a3c620b49", - "sha256:cc1ab6a25da197f03ebe6d8fa17273126120874386b4ac11c1d687df288542dd", - "sha256:dc3657869c7902810f32bd072f0740487f9e030c1a3ab03e0af093db35a9d14e", - "sha256:f5ec7740f9ccb90aec64edd71434711f58ee0ea7f5ed4ac48be11cfa9abf7317", - "sha256:fecb198dc389429be557cde50a2d46da8434a17fe37d7d41ff102e3987fd947b", - "sha256:ffa8f0966de2c22de408d0e322db2faed6f6e74265aa0856f3824813cf124363" + "sha256:021f09c15e1381e202d95d4a21ece8e7f2bf1388b6d7e9cae09dfe27bd2043d1", + "sha256:02954e285e8e2f4006b6f22be6f0df1f1c3c97adbb7ed211c6b483426f20d5c8", + "sha256:08d287b68fd28906a94564f15118a7ca8c242e50ae7f8bd91130c362b2108a81", + "sha256:24057459f19db9ebb02984c6fdd164a970b31a95f38e4a49cf7615b36a1b532c", + "sha256:25c9976c17311388fcd953cb3d0697999b2205333f4e11e669d90ff8d830d429", + "sha256:3223f997b6d2ebf9c010260cf3d889848a93f5d22bb4d14cd32638b3d8bba7ad", + "sha256:3f76280ce8ec216dde336e55b2b82e883401cf466da0fe3be317c03fb8ee7c7d", + "sha256:52867d69a54e71666cd184b04e839cff7dfc8ed0cd6b936995117fdae8790b69", + "sha256:52897edc2774d2779fbeb6880d2cfb305daa0b1a29c16b91f531a18918a6e0f3", + "sha256:5aa6b86802e8cf7716bf4b4b5a3c99b12d34e9c6a9d06dad254447a620437931", + "sha256:5b40c9f494e1f27588c369b9e4a6ca19cd924b3a0e1ef9ef1a8e30a07a438f43", + "sha256:65177d1c519b55e5b7f094c660ed357bb7d86e799686bb71653b8a4803d8ff0d", + "sha256:6ae6ffbd9d614c20d028c7117ee911fc4e266b4dca2065d5c5909e401f8ff683", + "sha256:7ad20d24acf3a0042512b7e8d8fdc2e827126ed519d6bd1ed8e6c14ec8a2c813", + "sha256:83c166b9bb27c1715bed94495d9598a7f02950b4749dba9349c1dd2cbf10729d", + "sha256:851b5afbb0d62f6129ae891b533aa508cc357d5892c240c91933d945fff15731", + "sha256:8d0382645ede2fde352da2a885aac28ec37d38587864c0689b4b2361d17b1d4c", + "sha256:a6cf8fcc8a63d333970b950a7331a30544cf59b1a97baf0a7409e09eafc1ac38", + "sha256:bbd98dcdcd32f408947afdb3f7434fade6edd408c3077bbce7bd840d654d92c6", + "sha256:d594e2ce51b8e0b4074e6644758865dc2bb13fd654450c1eae51201260a539f1", + "sha256:e78507adcc730533619de07bfdd1c62b2918a68cd4419ea386e28abf7f6a1e5c", + "sha256:e7f12b2de0060b0b858cfec0016e7d980ae5bae455a1746bfcc70929100ee633", + "sha256:e90c95abb3285d06f6e4feedafc134306a8eced93cb78e08cf50e224d5ce22e2", + "sha256:eff794eeb7883c5aefb1ed572e7ff533ae779f6c6277849eab9e77986e352688", + "sha256:fc4944dc004ca6cc701dfa19afb8bdb26ad36b9bed5bcec617d2a11e9cae6902" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==2.1.1" + "version": "==2.1.2" + }, + "phonenumberslite": { + "hashes": [ + "sha256:7c719e35ef551a895459382e9faf592f52647312dd90b543b06460aa0e1c49c4", + "sha256:cf6cf56c889c6787ec6b30b5791693f6dd678f633358f4aeea1fddf98d4cadcb" + ], + "version": "==8.13.23" }, "pillow": { "hashes": [ - "sha256:0462b1496505a3462d0f35dc1c4d7b54069747d65d00ef48e736acda2c8cbdff", - "sha256:186f7e04248103482ea6354af6d5bcedb62941ee08f7f788a1c7707bc720c66f", - "sha256:19e9adb3f22d4c416e7cd79b01375b17159d6990003633ff1d8377e21b7f1b21", - "sha256:28444cb6ad49726127d6b340217f0627abc8732f1194fd5352dec5e6a0105635", - "sha256:2872f2d7846cf39b3dbff64bc1104cc48c76145854256451d33c5faa55c04d1a", - "sha256:2cc6b86ece42a11f16f55fe8903595eff2b25e0358dec635d0a701ac9586588f", - "sha256:2d7e91b4379f7a76b31c2dda84ab9e20c6220488e50f7822e59dac36b0cd92b1", - "sha256:2fa6dd2661838c66f1a5473f3b49ab610c98a128fc08afbe81b91a1f0bf8c51d", - "sha256:32bec7423cdf25c9038fef614a853c9d25c07590e1a870ed471f47fb80b244db", - "sha256:3855447d98cced8670aaa63683808df905e956f00348732448b5a6df67ee5849", - "sha256:3a04359f308ebee571a3127fdb1bd01f88ba6f6fb6d087f8dd2e0d9bff43f2a7", - "sha256:3a0d3e54ab1df9df51b914b2233cf779a5a10dfd1ce339d0421748232cea9876", - "sha256:44e7e4587392953e5e251190a964675f61e4dae88d1e6edbe9f36d6243547ff3", - "sha256:459307cacdd4138edee3875bbe22a2492519e060660eaf378ba3b405d1c66317", - "sha256:4ce90f8a24e1c15465048959f1e94309dfef93af272633e8f37361b824532e91", - "sha256:50bd5f1ebafe9362ad622072a1d2f5850ecfa44303531ff14353a4059113b12d", - "sha256:522ff4ac3aaf839242c6f4e5b406634bfea002469656ae8358644fc6c4856a3b", - "sha256:552912dbca585b74d75279a7570dd29fa43b6d93594abb494ebb31ac19ace6bd", - "sha256:5d6c9049c6274c1bb565021367431ad04481ebb54872edecfcd6088d27edd6ed", - "sha256:697a06bdcedd473b35e50a7e7506b1d8ceb832dc238a336bd6f4f5aa91a4b500", - "sha256:71671503e3015da1b50bd18951e2f9daf5b6ffe36d16f1eb2c45711a301521a7", - "sha256:723bd25051454cea9990203405fa6b74e043ea76d4968166dfd2569b0210886a", - "sha256:764d2c0daf9c4d40ad12fbc0abd5da3af7f8aa11daf87e4fa1b834000f4b6b0a", - "sha256:787bb0169d2385a798888e1122c980c6eff26bf941a8ea79747d35d8f9210ca0", - "sha256:7f771e7219ff04b79e231d099c0a28ed83aa82af91fd5fa9fdb28f5b8d5addaf", - "sha256:847e8d1017c741c735d3cd1883fa7b03ded4f825a6e5fcb9378fd813edee995f", - "sha256:84efb46e8d881bb06b35d1d541aa87f574b58e87f781cbba8d200daa835b42e1", - "sha256:898f1d306298ff40dc1b9ca24824f0488f6f039bc0e25cfb549d3195ffa17088", - "sha256:8b451d6ead6e3500b6ce5c7916a43d8d8d25ad74b9102a629baccc0808c54971", - "sha256:8f06be50669087250f319b706decf69ca71fdecd829091a37cc89398ca4dc17a", - "sha256:92a23b0431941a33242b1f0ce6c88a952e09feeea9af4e8be48236a68ffe2205", - "sha256:93139acd8109edcdeffd85e3af8ae7d88b258b3a1e13a038f542b79b6d255c54", - "sha256:98533fd7fa764e5f85eebe56c8e4094db912ccbe6fbf3a58778d543cadd0db08", - "sha256:9f665d1e6474af9f9da5e86c2a3a2d2d6204e04d5af9c06b9d42afa6ebde3f21", - "sha256:b059ac2c4c7a97daafa7dc850b43b2d3667def858a4f112d1aa082e5c3d6cf7d", - "sha256:b1be1c872b9b5fcc229adeadbeb51422a9633abd847c0ff87dc4ef9bb184ae08", - "sha256:b7cf63d2c6928b51d35dfdbda6f2c1fddbe51a6bc4a9d4ee6ea0e11670dd981e", - "sha256:bc2e3069569ea9dbe88d6b8ea38f439a6aad8f6e7a6283a38edf61ddefb3a9bf", - "sha256:bcf1207e2f2385a576832af02702de104be71301c2696d0012b1b93fe34aaa5b", - "sha256:ca26ba5767888c84bf5a0c1a32f069e8204ce8c21d00a49c90dabeba00ce0145", - "sha256:cbe68deb8580462ca0d9eb56a81912f59eb4542e1ef8f987405e35a0179f4ea2", - "sha256:d6caf3cd38449ec3cd8a68b375e0c6fe4b6fd04edb6c9766b55ef84a6e8ddf2d", - "sha256:d72967b06be9300fed5cfbc8b5bafceec48bf7cdc7dab66b1d2549035287191d", - "sha256:d889b53ae2f030f756e61a7bff13684dcd77e9af8b10c6048fb2c559d6ed6eaf", - "sha256:de596695a75496deb3b499c8c4f8e60376e0516e1a774e7bc046f0f48cd620ad", - "sha256:e6a90167bcca1216606223a05e2cf991bb25b14695c518bc65639463d7db722d", - "sha256:ed2d9c0704f2dc4fa980b99d565c0c9a543fe5101c25b3d60488b8ba80f0cce1", - "sha256:ee7810cf7c83fa227ba9125de6084e5e8b08c59038a7b2c9045ef4dde61663b4", - "sha256:f0b4b06da13275bc02adfeb82643c4a6385bd08d26f03068c2796f60d125f6f2", - "sha256:f11c9102c56ffb9ca87134bd025a43d2aba3f1155f508eff88f694b33a9c6d19", - "sha256:f5bb289bb835f9fe1a1e9300d011eef4d69661bb9b34d5e196e5e82c4cb09b37", - "sha256:f6d3d4c905e26354e8f9d82548475c46d8e0889538cb0657aa9c6f0872a37aa4", - "sha256:fcb59711009b0168d6ee0bd8fb5eb259c4ab1717b2f538bbf36bacf207ef7a68", - "sha256:fd2a5403a75b54661182b75ec6132437a181209b901446ee5724b589af8edef1" + "sha256:00f438bb841382b15d7deb9a05cc946ee0f2c352653c7aa659e75e592f6fa17d", + "sha256:0248f86b3ea061e67817c47ecbe82c23f9dd5d5226200eb9090b3873d3ca32de", + "sha256:04f6f6149f266a100374ca3cc368b67fb27c4af9f1cc8cb6306d849dcdf12616", + "sha256:062a1610e3bc258bff2328ec43f34244fcec972ee0717200cb1425214fe5b839", + "sha256:0a026c188be3b443916179f5d04548092e253beb0c3e2ee0a4e2cdad72f66099", + "sha256:0f7c276c05a9767e877a0b4c5050c8bee6a6d960d7f0c11ebda6b99746068c2a", + "sha256:1a8413794b4ad9719346cd9306118450b7b00d9a15846451549314a58ac42219", + "sha256:1ab05f3db77e98f93964697c8efc49c7954b08dd61cff526b7f2531a22410106", + "sha256:1c3ac5423c8c1da5928aa12c6e258921956757d976405e9467c5f39d1d577a4b", + "sha256:1c41d960babf951e01a49c9746f92c5a7e0d939d1652d7ba30f6b3090f27e412", + "sha256:1fafabe50a6977ac70dfe829b2d5735fd54e190ab55259ec8aea4aaea412fa0b", + "sha256:1fb29c07478e6c06a46b867e43b0bcdb241b44cc52be9bc25ce5944eed4648e7", + "sha256:24fadc71218ad2b8ffe437b54876c9382b4a29e030a05a9879f615091f42ffc2", + "sha256:2cdc65a46e74514ce742c2013cd4a2d12e8553e3a2563c64879f7c7e4d28bce7", + "sha256:2ef6721c97894a7aa77723740a09547197533146fba8355e86d6d9a4a1056b14", + "sha256:3b834f4b16173e5b92ab6566f0473bfb09f939ba14b23b8da1f54fa63e4b623f", + "sha256:3d929a19f5469b3f4df33a3df2983db070ebb2088a1e145e18facbc28cae5b27", + "sha256:41f67248d92a5e0a2076d3517d8d4b1e41a97e2df10eb8f93106c89107f38b57", + "sha256:47e5bf85b80abc03be7455c95b6d6e4896a62f6541c1f2ce77a7d2bb832af262", + "sha256:4d0152565c6aa6ebbfb1e5d8624140a440f2b99bf7afaafbdbf6430426497f28", + "sha256:50d08cd0a2ecd2a8657bd3d82c71efd5a58edb04d9308185d66c3a5a5bed9610", + "sha256:61f1a9d247317fa08a308daaa8ee7b3f760ab1809ca2da14ecc88ae4257d6172", + "sha256:6932a7652464746fcb484f7fc3618e6503d2066d853f68a4bd97193a3996e273", + "sha256:7a7e3daa202beb61821c06d2517428e8e7c1aab08943e92ec9e5755c2fc9ba5e", + "sha256:7dbaa3c7de82ef37e7708521be41db5565004258ca76945ad74a8e998c30af8d", + "sha256:7df5608bc38bd37ef585ae9c38c9cd46d7c81498f086915b0f97255ea60c2818", + "sha256:806abdd8249ba3953c33742506fe414880bad78ac25cc9a9b1c6ae97bedd573f", + "sha256:883f216eac8712b83a63f41b76ddfb7b2afab1b74abbb413c5df6680f071a6b9", + "sha256:912e3812a1dbbc834da2b32299b124b5ddcb664ed354916fd1ed6f193f0e2d01", + "sha256:937bdc5a7f5343d1c97dc98149a0be7eb9704e937fe3dc7140e229ae4fc572a7", + "sha256:9882a7451c680c12f232a422730f986a1fcd808da0fd428f08b671237237d651", + "sha256:9a92109192b360634a4489c0c756364c0c3a2992906752165ecb50544c251312", + "sha256:9d7bc666bd8c5a4225e7ac71f2f9d12466ec555e89092728ea0f5c0c2422ea80", + "sha256:a5f63b5a68daedc54c7c3464508d8c12075e56dcfbd42f8c1bf40169061ae666", + "sha256:a646e48de237d860c36e0db37ecaecaa3619e6f3e9d5319e527ccbc8151df061", + "sha256:a89b8312d51715b510a4fe9fc13686283f376cfd5abca8cd1c65e4c76e21081b", + "sha256:a92386125e9ee90381c3369f57a2a50fa9e6aa8b1cf1d9c4b200d41a7dd8e992", + "sha256:ae88931f93214777c7a3aa0a8f92a683f83ecde27f65a45f95f22d289a69e593", + "sha256:afc8eef765d948543a4775f00b7b8c079b3321d6b675dde0d02afa2ee23000b4", + "sha256:b0eb01ca85b2361b09480784a7931fc648ed8b7836f01fb9241141b968feb1db", + "sha256:b1c25762197144e211efb5f4e8ad656f36c8d214d390585d1d21281f46d556ba", + "sha256:b4005fee46ed9be0b8fb42be0c20e79411533d1fd58edabebc0dd24626882cfd", + "sha256:b920e4d028f6442bea9a75b7491c063f0b9a3972520731ed26c83e254302eb1e", + "sha256:baada14941c83079bf84c037e2d8b7506ce201e92e3d2fa0d1303507a8538212", + "sha256:bb40c011447712d2e19cc261c82655f75f32cb724788df315ed992a4d65696bb", + "sha256:c0949b55eb607898e28eaccb525ab104b2d86542a85c74baf3a6dc24002edec2", + "sha256:c9aeea7b63edb7884b031a35305629a7593272b54f429a9869a4f63a1bf04c34", + "sha256:cfe96560c6ce2f4c07d6647af2d0f3c54cc33289894ebd88cfbb3bcd5391e256", + "sha256:d27b5997bdd2eb9fb199982bb7eb6164db0426904020dc38c10203187ae2ff2f", + "sha256:d921bc90b1defa55c9917ca6b6b71430e4286fc9e44c55ead78ca1a9f9eba5f2", + "sha256:e6bf8de6c36ed96c86ea3b6e1d5273c53f46ef518a062464cd7ef5dd2cf92e38", + "sha256:eaed6977fa73408b7b8a24e8b14e59e1668cfc0f4c40193ea7ced8e210adf996", + "sha256:fa1d323703cfdac2036af05191b969b910d8f115cf53093125e4058f62012c9a", + "sha256:fe1e26e1ffc38be097f0ba1d0d07fcade2bcfd1d023cda5b29935ae8052bd793" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==10.0.1" + "version": "==10.1.0" }, "prompt-toolkit": { "hashes": [ @@ -628,21 +646,23 @@ }, "psycopg2": { "hashes": [ - "sha256:287a64ef168ef7fb9f382964705ff664b342bfff47e7242bf0a04ef203269dd5", - "sha256:2f8594f92bbb5d8b59ffec04e2686c416401e2d4297de1193f8e75235937e71d", - "sha256:3da6488042a53b50933244085f3f91803f1b7271f970f3e5536efa69314f6a49", - "sha256:65f81e72136d8b9ac8abf5206938d60f50da424149a43b6073f1546063c0565e", - "sha256:67c2f32f3aba79afb15799575e77ee2db6b46b8acf943c21d34d02d4e1041d50", - "sha256:81b21424023a290a40884c7f8b0093ba6465b59bd785c18f757e76945f65594c", - "sha256:d39bb3959788b2c9d7bf5ff762e29f436172b241cd7b47529baac77746fd7918", - "sha256:d4ad050ea50a16731d219c3a85e8f2debf49415a070f0b8331ccc96c81700d9b", - "sha256:dcde3cad4920e29e74bf4e76c072649764914facb2069e6b7fa1ddbebcd49e9f", - "sha256:f7e62095d749359b7854143843f27edd7dccfcd3e1d833b880562aa5702d92b0", - "sha256:f9ecbf504c4eaff90139d5c9b95d47275f2b2651e14eba56392b4041fbf4c2b3" + "sha256:121081ea2e76729acfb0673ff33755e8703d45e926e416cb59bae3a86c6a4981", + "sha256:38a8dcc6856f569068b47de286b472b7c473ac7977243593a288ebce0dc89516", + "sha256:426f9f29bde126913a20a96ff8ce7d73fd8a216cfb323b1f04da402d452853c3", + "sha256:5e0d98cade4f0e0304d7d6f25bbfbc5bd186e07b38eac65379309c4ca3193efa", + "sha256:7e2dacf8b009a1c1e843b5213a87f7c544b2b042476ed7755be813eaf4e8347a", + "sha256:a7653d00b732afb6fc597e29c50ad28087dcb4fbfb28e86092277a559ae4e693", + "sha256:ade01303ccf7ae12c356a5e10911c9e1c51136003a9a1d92f7aa9d010fb98372", + "sha256:bac58c024c9922c23550af2a581998624d6e02350f4ae9c5f0bc642c633a2d5e", + "sha256:c92811b2d4c9b6ea0285942b2e7cac98a59e166d59c588fe5cfe1eda58e72d59", + "sha256:d1454bde93fb1e224166811694d600e746430c006fbb031ea06ecc2ea41bf156", + "sha256:d735786acc7dd25815e89cc4ad529a43af779db2e25aa7c626de864127e5a024", + "sha256:de80739447af31525feddeb8effd640782cf5998e1a4e9192ebdf829717e3913", + "sha256:ff432630e510709564c01dafdbe996cb552e0b9f3f065eb89bdce5bd31fabf4c" ], "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==2.9.8" + "markers": "python_version >= '3.7'", + "version": "==2.9.9" }, "pycparser": { "hashes": [ @@ -766,11 +786,11 @@ }, "sentry-sdk": { "hashes": [ - "sha256:64a7141005fb775b9db298a30de93e3b83e0ddd1232dc6f36eb38aebc1553291", - "sha256:6de2e88304873484207fed836388e422aeff000609b104c802749fd89d56ba5b" + "sha256:935e8fbd7787a3702457393b74b13d89a5afb67185bc0af85c00cb27cbd42e7c", + "sha256:eeb0b3550536f3bbc05bb1c7e0feb3a78d74acb43b607159a606ed2ec0a33a4d" ], "index": "pypi", - "version": "==1.31.0" + "version": "==1.32.0" }, "six": { "hashes": [ @@ -823,11 +843,11 @@ }, "urllib3": { "hashes": [ - "sha256:24d6a242c28d29af46c3fae832c36db3bbebcc533dd1bb549172cd739c82df21", - "sha256:94a757d178c9be92ef5539b8840d48dc9cf1b2709c9d6b588232a055c524458b" + "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07", + "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0" ], "markers": "python_version >= '3.6'", - "version": "==1.26.17" + "version": "==1.26.18" }, "uwsgi": { "hashes": [ @@ -1043,12 +1063,12 @@ }, "django": { "hashes": [ - "sha256:a5de4c484e7b7418e6d3e52a5b8794f0e6b9f9e4ce3c037018cf1c489fa87f3c", - "sha256:d31b06c58aa2cd73998ca5966bc3001243d3c4e77ee2d0c479bced124765fd99" + "sha256:83b6d66b06e484807d778263fdc7f9186d4dc1862fcfa6507830446ac6b060ba", + "sha256:c5e7b668025a6e06cad9ba6d4de1fd1a21212acebb51ea34abb400c6e4d33430" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==3.2.21" + "version": "==3.2.22" }, "django-extensions": { "hashes": [ @@ -1078,11 +1098,12 @@ }, "flake8-absolute-import": { "hashes": [ - "sha256:d24f189bca52ffc0d13e8046606ea42d22a9ad9d409bf39e52b93493cf2ffd2c" + "sha256:b72142db999ec5e0ac4f4ac57fb8776a2959d07346c4d3742c446f206d45fcef", + "sha256:fcb734ac5a9639fa4ffbc6242ae9d6e9d8063f9cd078d6d218597ee883a99d48" ], "index": "pypi", - "markers": "python_version >= '3.4'", - "version": "==1.0.0.1" + "markers": "python_version >= '3.6'", + "version": "==1.0.0.2" }, "flake8-isort": { "hashes": [ @@ -1254,11 +1275,11 @@ }, "pycodestyle": { "hashes": [ - "sha256:259bcc17857d8a8b3b4a2327324b79e5f020a13c16074670f9c8c8f872ea76d0", - "sha256:5d1013ba8dc7895b548be5afb05740ca82454fd899971563d2ef625d090326f8" + "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f", + "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67" ], "markers": "python_version >= '3.8'", - "version": "==2.11.0" + "version": "==2.11.1" }, "pyflakes": { "hashes": [ @@ -1270,117 +1291,117 @@ }, "pytest": { "hashes": [ - "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002", - "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069" + "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac", + "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==7.4.2" + "version": "==7.4.3" }, "pytoolconfig": { "extras": [ "global" ], "hashes": [ - "sha256:239ba9d3e537b91d0243275a497700ea39a5e259ddb80421c366e3b288bf30fe", - "sha256:a50f9dfe23b03a9d40414c1fdf902fefbeae12f2ac75a3c8f915944d6ffac279" + "sha256:e8b2e538f11dbabc4617884d45401e0105e2d7db920cb8ae6baa94d66126a8e3", + "sha256:f2d00ea4f8cbdffd3006780ba51016618c835b338f634e3f7f8b2715b1710889" ], "markers": "python_version >= '3.7'", - "version": "==1.2.5" + "version": "==1.2.6" }, "regex": { "hashes": [ - "sha256:0085da0f6c6393428bf0d9c08d8b1874d805bb55e17cb1dfa5ddb7cfb11140bf", - "sha256:06c57e14ac723b04458df5956cfb7e2d9caa6e9d353c0b4c7d5d54fcb1325c46", - "sha256:09b7f4c66aa9d1522b06e31a54f15581c37286237208df1345108fcf4e050c18", - "sha256:0c59122ceccb905a941fb23b087b8eafc5290bf983ebcb14d2301febcbe199c7", - "sha256:1005c60ed7037be0d9dea1f9c53cc42f836188227366370867222bda4c3c6bd7", - "sha256:14898830f0a0eb67cae2bbbc787c1a7d6e34ecc06fbd39d3af5fe29a4468e2c9", - "sha256:14dc6f2d88192a67d708341f3085df6a4f5a0c7b03dec08d763ca2cd86e9f559", - "sha256:1e7d84d64c84ad97bf06f3c8cb5e48941f135ace28f450d86af6b6512f1c9a71", - "sha256:2162ae2eb8b079622176a81b65d486ba50b888271302190870b8cc488587d280", - "sha256:22283c769a7b01c8ac355d5be0715bf6929b6267619505e289f792b01304d898", - "sha256:239c3c2a339d3b3ddd51c2daef10874410917cd2b998f043c13e2084cb191684", - "sha256:293352710172239bf579c90a9864d0df57340b6fd21272345222fb6371bf82b3", - "sha256:2ae54a338191e1356253e7883d9d19f8679b6143703086245fb14d1f20196be9", - "sha256:2e73e5243af12d9cd6a9d6a45a43570dbe2e5b1cdfc862f5ae2b031e44dd95a8", - "sha256:2e9216e0d2cdce7dbc9be48cb3eacb962740a09b011a116fd7af8c832ab116ca", - "sha256:3026cbcf11d79095a32d9a13bbc572a458727bd5b1ca332df4a79faecd45281c", - "sha256:3611576aff55918af2697410ff0293d6071b7e00f4b09e005d614686ac4cd57c", - "sha256:3ae646c35cb9f820491760ac62c25b6d6b496757fda2d51be429e0e7b67ae0ab", - "sha256:3b8e6ea6be6d64104d8e9afc34c151926f8182f84e7ac290a93925c0db004bfd", - "sha256:3d370ff652323c5307d9c8e4c62efd1956fb08051b0e9210212bc51168b4ff56", - "sha256:3f7454aa427b8ab9101f3787eb178057c5250478e39b99540cfc2b889c7d0586", - "sha256:40f029d73b10fac448c73d6eb33d57b34607f40116e9f6e9f0d32e9229b147d7", - "sha256:423adfa872b4908843ac3e7a30f957f5d5282944b81ca0a3b8a7ccbbfaa06103", - "sha256:4873ef92e03a4309b3ccd8281454801b291b689f6ad45ef8c3658b6fa761d7ac", - "sha256:48c640b99213643d141550326f34f0502fedb1798adb3c9eb79650b1ecb2f177", - "sha256:4ae594c66f4a7e1ea67232a0846649a7c94c188d6c071ac0210c3e86a5f92109", - "sha256:4b694430b3f00eb02c594ff5a16db30e054c1b9589a043fe9174584c6efa8033", - "sha256:51d8ea2a3a1a8fe4f67de21b8b93757005213e8ac3917567872f2865185fa7fb", - "sha256:54de2619f5ea58474f2ac211ceea6b615af2d7e4306220d4f3fe690c91988a61", - "sha256:551ad543fa19e94943c5b2cebc54c73353ffff08228ee5f3376bd27b3d5b9800", - "sha256:5543c055d8ec7801901e1193a51570643d6a6ab8751b1f7dd9af71af467538bb", - "sha256:5cd9cd7170459b9223c5e592ac036e0704bee765706445c353d96f2890e816c8", - "sha256:5ec4b3f0aebbbe2fc0134ee30a791af522a92ad9f164858805a77442d7d18570", - "sha256:67ecd894e56a0c6108ec5ab1d8fa8418ec0cff45844a855966b875d1039a2e34", - "sha256:6ab2ed84bf0137927846b37e882745a827458689eb969028af8032b1b3dac78e", - "sha256:704f63b774218207b8ccc6c47fcef5340741e5d839d11d606f70af93ee78e4d4", - "sha256:7098c524ba9f20717a56a8d551d2ed491ea89cbf37e540759ed3b776a4f8d6eb", - "sha256:7aed90a72fc3654fba9bc4b7f851571dcc368120432ad68b226bd593f3f6c0b7", - "sha256:7ce606c14bb195b0e5108544b540e2c5faed6843367e4ab3deb5c6aa5e681208", - "sha256:7eb95fe8222932c10d4436e7a6f7c99991e3fdd9f36c949eff16a69246dee2dc", - "sha256:80b80b889cb767cc47f31d2b2f3dec2db8126fbcd0cff31b3925b4dc6609dcdb", - "sha256:82cd0a69cd28f6cc3789cc6adeb1027f79526b1ab50b1f6062bbc3a0ccb2dbc3", - "sha256:83215147121e15d5f3a45d99abeed9cf1fe16869d5c233b08c56cdf75f43a504", - "sha256:88900f521c645f784260a8d346e12a1590f79e96403971241e64c3a265c8ecdb", - "sha256:91129ff1bb0619bc1f4ad19485718cc623a2dc433dff95baadbf89405c7f6b57", - "sha256:920974009fb37b20d32afcdf0227a2e707eb83fe418713f7a8b7de038b870d0b", - "sha256:9233ac249b354c54146e392e8a451e465dd2d967fc773690811d3a8c240ac601", - "sha256:941460db8fe3bd613db52f05259c9336f5a47ccae7d7def44cc277184030a116", - "sha256:942f8b1f3b223638b02df7df79140646c03938d488fbfb771824f3d05fc083a8", - "sha256:964b16dcc10c79a4a2be9f1273fcc2684a9eedb3906439720598029a797b46e6", - "sha256:9691a549c19c22d26a4f3b948071e93517bdf86e41b81d8c6ac8a964bb71e5a6", - "sha256:96979d753b1dc3b2169003e1854dc67bfc86edf93c01e84757927f810b8c3c93", - "sha256:987b9ac04d0b38ef4f89fbc035e84a7efad9cdd5f1e29024f9289182c8d99e09", - "sha256:988631b9d78b546e284478c2ec15c8a85960e262e247b35ca5eaf7ee22f6050a", - "sha256:9a96edd79661e93327cfeac4edec72a4046e14550a1d22aa0dd2e3ca52aec921", - "sha256:9b7408511fca48a82a119d78a77c2f5eb1b22fe88b0d2450ed0756d194fe7a9a", - "sha256:9dd6082f4e2aec9b6a0927202c85bc1b09dcab113f97265127c1dc20e2e32495", - "sha256:a2ad5add903eb7cdde2b7c64aaca405f3957ab34f16594d2b78d53b8b1a6a7d6", - "sha256:a8c65c17aed7e15a0c824cdc63a6b104dfc530f6fa8cb6ac51c437af52b481c7", - "sha256:aadf28046e77a72f30dcc1ab185639e8de7f4104b8cb5c6dfa5d8ed860e57236", - "sha256:b076da1ed19dc37788f6a934c60adf97bd02c7eea461b73730513921a85d4235", - "sha256:b2aeab3895d778155054abea5238d0eb9a72e9242bd4b43f42fd911ef9a13470", - "sha256:b82edc98d107cbc7357da7a5a695901b47d6eb0420e587256ba3ad24b80b7d0b", - "sha256:b8a0ccc8f2698f120e9e5742f4b38dc944c38744d4bdfc427616f3a163dd9de5", - "sha256:b993b6f524d1e274a5062488a43e3f9f8764ee9745ccd8e8193df743dbe5ee61", - "sha256:bb34d1605f96a245fc39790a117ac1bac8de84ab7691637b26ab2c5efb8f228c", - "sha256:bd3366aceedf274f765a3a4bc95d6cd97b130d1dda524d8f25225d14123c01db", - "sha256:c12f6f67495ea05c3d542d119d270007090bad5b843f642d418eb601ec0fa7be", - "sha256:c662a4cbdd6280ee56f841f14620787215a171c4e2d1744c9528bed8f5816c96", - "sha256:c884d1a59e69e03b93cf0dfee8794c63d7de0ee8f7ffb76e5f75be8131b6400a", - "sha256:ca339088839582d01654e6f83a637a4b8194d0960477b9769d2ff2cfa0fa36d2", - "sha256:cd2b6c5dfe0929b6c23dde9624483380b170b6e34ed79054ad131b20203a1a63", - "sha256:ce0f9fbe7d295f9922c0424a3637b88c6c472b75eafeaff6f910494a1fa719ef", - "sha256:cf0633e4a1b667bfe0bb10b5e53fe0d5f34a6243ea2530eb342491f1adf4f739", - "sha256:cf9273e96f3ee2ac89ffcb17627a78f78e7516b08f94dc435844ae72576a276e", - "sha256:d909b5a3fff619dc7e48b6b1bedc2f30ec43033ba7af32f936c10839e81b9217", - "sha256:d9b6627408021452dcd0d2cdf8da0534e19d93d070bfa8b6b4176f99711e7f90", - "sha256:de35342190deb7b866ad6ba5cbcccb2d22c0487ee0cbb251efef0843d705f0d4", - "sha256:e51c80c168074faa793685656c38eb7a06cbad7774c8cbc3ea05552d615393d8", - "sha256:e6bd1e9b95bc5614a7a9c9c44fde9539cba1c823b43a9f7bc11266446dd568e3", - "sha256:e7a9aaa5a1267125eef22cef3b63484c3241aaec6f48949b366d26c7250e0357", - "sha256:e951d1a8e9963ea51efd7f150450803e3b95db5939f994ad3d5edac2b6f6e2b4", - "sha256:e9941a4ada58f6218694f382e43fdd256e97615db9da135e77359da257a7168b", - "sha256:f0640913d2c1044d97e30d7c41728195fc37e54d190c5385eacb52115127b882", - "sha256:f0ccf3e01afeb412a1a9993049cb160d0352dba635bbca7762b2dc722aa5742a", - "sha256:f2181c20ef18747d5f4a7ea513e09ea03bdd50884a11ce46066bb90fe4213675", - "sha256:f2200e00b62568cfd920127782c61bc1c546062a879cdc741cfcc6976668dfcf", - "sha256:fcbdc5f2b0f1cd0f6a56cdb46fe41d2cce1e644e3b68832f3eeebc5fb0f7712e" + "sha256:00ba3c9818e33f1fa974693fb55d24cdc8ebafcb2e4207680669d8f8d7cca79a", + "sha256:00e871d83a45eee2f8688d7e6849609c2ca2a04a6d48fba3dff4deef35d14f07", + "sha256:06e9abc0e4c9ab4779c74ad99c3fc10d3967d03114449acc2c2762ad4472b8ca", + "sha256:0b9ac09853b2a3e0d0082104036579809679e7715671cfbf89d83c1cb2a30f58", + "sha256:0d47840dc05e0ba04fe2e26f15126de7c755496d5a8aae4a08bda4dd8d646c54", + "sha256:0f649fa32fe734c4abdfd4edbb8381c74abf5f34bc0b3271ce687b23729299ed", + "sha256:107ac60d1bfdc3edb53be75e2a52aff7481b92817cfdddd9b4519ccf0e54a6ff", + "sha256:11175910f62b2b8c055f2b089e0fedd694fe2be3941b3e2633653bc51064c528", + "sha256:12bd4bc2c632742c7ce20db48e0d99afdc05e03f0b4c1af90542e05b809a03d9", + "sha256:16f8740eb6dbacc7113e3097b0a36065a02e37b47c936b551805d40340fb9971", + "sha256:1c0e8fae5b27caa34177bdfa5a960c46ff2f78ee2d45c6db15ae3f64ecadde14", + "sha256:2c54e23836650bdf2c18222c87f6f840d4943944146ca479858404fedeb9f9af", + "sha256:3367007ad1951fde612bf65b0dffc8fd681a4ab98ac86957d16491400d661302", + "sha256:36362386b813fa6c9146da6149a001b7bd063dabc4d49522a1f7aa65b725c7ec", + "sha256:39807cbcbe406efca2a233884e169d056c35aa7e9f343d4e78665246a332f597", + "sha256:39cdf8d141d6d44e8d5a12a8569d5a227f645c87df4f92179bd06e2e2705e76b", + "sha256:3b2c3502603fab52d7619b882c25a6850b766ebd1b18de3df23b2f939360e1bd", + "sha256:3ccf2716add72f80714b9a63899b67fa711b654be3fcdd34fa391d2d274ce767", + "sha256:3fef4f844d2290ee0ba57addcec17eec9e3df73f10a2748485dfd6a3a188cc0f", + "sha256:4023e2efc35a30e66e938de5aef42b520c20e7eda7bb5fb12c35e5d09a4c43f6", + "sha256:4a3ee019a9befe84fa3e917a2dd378807e423d013377a884c1970a3c2792d293", + "sha256:4a8bf76e3182797c6b1afa5b822d1d5802ff30284abe4599e1247be4fd6b03be", + "sha256:4a992f702c9be9c72fa46f01ca6e18d131906a7180950958f766c2aa294d4b41", + "sha256:4c34d4f73ea738223a094d8e0ffd6d2c1a1b4c175da34d6b0de3d8d69bee6bcc", + "sha256:4cd1bccf99d3ef1ab6ba835308ad85be040e6a11b0977ef7ea8c8005f01a3c29", + "sha256:4ef80829117a8061f974b2fda8ec799717242353bff55f8a29411794d635d964", + "sha256:58837f9d221744d4c92d2cf7201c6acd19623b50c643b56992cbd2b745485d3d", + "sha256:5a8f91c64f390ecee09ff793319f30a0f32492e99f5dc1c72bc361f23ccd0a9a", + "sha256:5addc9d0209a9afca5fc070f93b726bf7003bd63a427f65ef797a931782e7edc", + "sha256:6239d4e2e0b52c8bd38c51b760cd870069f0bdf99700a62cd509d7a031749a55", + "sha256:66e2fe786ef28da2b28e222c89502b2af984858091675044d93cb50e6f46d7af", + "sha256:69c0771ca5653c7d4b65203cbfc5e66db9375f1078689459fe196fe08b7b4930", + "sha256:6ac965a998e1388e6ff2e9781f499ad1eaa41e962a40d11c7823c9952c77123e", + "sha256:6c56c3d47da04f921b73ff9415fbaa939f684d47293f071aa9cbb13c94afc17d", + "sha256:6f85739e80d13644b981a88f529d79c5bdf646b460ba190bffcaf6d57b2a9863", + "sha256:706e7b739fdd17cb89e1fbf712d9dc21311fc2333f6d435eac2d4ee81985098c", + "sha256:741ba2f511cc9626b7561a440f87d658aabb3d6b744a86a3c025f866b4d19e7f", + "sha256:7434a61b158be563c1362d9071358f8ab91b8d928728cd2882af060481244c9e", + "sha256:76066d7ff61ba6bf3cb5efe2428fc82aac91802844c022d849a1f0f53820502d", + "sha256:7979b834ec7a33aafae34a90aad9f914c41fd6eaa8474e66953f3f6f7cbd4368", + "sha256:7eece6fbd3eae4a92d7c748ae825cbc1ee41a89bb1c3db05b5578ed3cfcfd7cb", + "sha256:7ef1e014eed78ab650bef9a6a9cbe50b052c0aebe553fb2881e0453717573f52", + "sha256:81dce2ddc9f6e8f543d94b05d56e70d03a0774d32f6cca53e978dc01e4fc75b8", + "sha256:82fcc1f1cc3ff1ab8a57ba619b149b907072e750815c5ba63e7aa2e1163384a4", + "sha256:8d1f21af4c1539051049796a0f50aa342f9a27cde57318f2fc41ed50b0dbc4ac", + "sha256:90a79bce019c442604662d17bf69df99090e24cdc6ad95b18b6725c2988a490e", + "sha256:9145f092b5d1977ec8c0ab46e7b3381b2fd069957b9862a43bd383e5c01d18c2", + "sha256:91dc1d531f80c862441d7b66c4505cd6ea9d312f01fb2f4654f40c6fdf5cc37a", + "sha256:979c24cbefaf2420c4e377ecd1f165ea08cc3d1fbb44bdc51bccbbf7c66a2cb4", + "sha256:994645a46c6a740ee8ce8df7911d4aee458d9b1bc5639bc968226763d07f00fa", + "sha256:9b98b7681a9437262947f41c7fac567c7e1f6eddd94b0483596d320092004533", + "sha256:9c6b4d23c04831e3ab61717a707a5d763b300213db49ca680edf8bf13ab5d91b", + "sha256:9c6d0ced3c06d0f183b73d3c5920727268d2201aa0fe6d55c60d68c792ff3588", + "sha256:9fd88f373cb71e6b59b7fa597e47e518282455c2734fd4306a05ca219a1991b0", + "sha256:a8f4e49fc3ce020f65411432183e6775f24e02dff617281094ba6ab079ef0915", + "sha256:a9e908ef5889cda4de038892b9accc36d33d72fb3e12c747e2799a0e806ec841", + "sha256:ad08a69728ff3c79866d729b095872afe1e0557251da4abb2c5faff15a91d19a", + "sha256:adbccd17dcaff65704c856bd29951c58a1bd4b2b0f8ad6b826dbd543fe740988", + "sha256:b0c7d2f698e83f15228ba41c135501cfe7d5740181d5903e250e47f617eb4292", + "sha256:b3ab05a182c7937fb374f7e946f04fb23a0c0699c0450e9fb02ef567412d2fa3", + "sha256:b6104f9a46bd8743e4f738afef69b153c4b8b592d35ae46db07fc28ae3d5fb7c", + "sha256:ba7cd6dc4d585ea544c1412019921570ebd8a597fabf475acc4528210d7c4a6f", + "sha256:bc72c231f5449d86d6c7d9cc7cd819b6eb30134bb770b8cfdc0765e48ef9c420", + "sha256:bce8814b076f0ce5766dc87d5a056b0e9437b8e0cd351b9a6c4e1134a7dfbda9", + "sha256:be5e22bbb67924dea15039c3282fa4cc6cdfbe0cbbd1c0515f9223186fc2ec5f", + "sha256:be6b7b8d42d3090b6c80793524fa66c57ad7ee3fe9722b258aec6d0672543fd0", + "sha256:bfe50b61bab1b1ec260fa7cd91106fa9fece57e6beba05630afe27c71259c59b", + "sha256:bff507ae210371d4b1fe316d03433ac099f184d570a1a611e541923f78f05037", + "sha256:c148bec483cc4b421562b4bcedb8e28a3b84fcc8f0aa4418e10898f3c2c0eb9b", + "sha256:c15ad0aee158a15e17e0495e1e18741573d04eb6da06d8b84af726cfc1ed02ee", + "sha256:c2169b2dcabf4e608416f7f9468737583ce5f0a6e8677c4efbf795ce81109d7c", + "sha256:c55853684fe08d4897c37dfc5faeff70607a5f1806c8be148f1695be4a63414b", + "sha256:c65a3b5330b54103e7d21cac3f6bf3900d46f6d50138d73343d9e5b2900b2353", + "sha256:c7964c2183c3e6cce3f497e3a9f49d182e969f2dc3aeeadfa18945ff7bdd7051", + "sha256:cc3f1c053b73f20c7ad88b0d1d23be7e7b3901229ce89f5000a8399746a6e039", + "sha256:ce615c92d90df8373d9e13acddd154152645c0dc060871abf6bd43809673d20a", + "sha256:d29338556a59423d9ff7b6eb0cb89ead2b0875e08fe522f3e068b955c3e7b59b", + "sha256:d8a993c0a0ffd5f2d3bda23d0cd75e7086736f8f8268de8a82fbc4bd0ac6791e", + "sha256:d9c727bbcf0065cbb20f39d2b4f932f8fa1631c3e01fcedc979bd4f51fe051c5", + "sha256:dac37cf08fcf2094159922edc7a2784cfcc5c70f8354469f79ed085f0328ebdf", + "sha256:dd829712de97753367153ed84f2de752b86cd1f7a88b55a3a775eb52eafe8a94", + "sha256:e54ddd0bb8fb626aa1f9ba7b36629564544954fff9669b15da3610c22b9a0991", + "sha256:e77c90ab5997e85901da85131fd36acd0ed2221368199b65f0d11bca44549711", + "sha256:ebedc192abbc7fd13c5ee800e83a6df252bec691eb2c4bedc9f8b2e2903f5e2a", + "sha256:ef71561f82a89af6cfcbee47f0fabfdb6e63788a9258e913955d89fdd96902ab", + "sha256:f0a47efb1dbef13af9c9a54a94a0b814902e547b7f21acb29434504d18f36e3a", + "sha256:f4f2ca6df64cbdd27f27b34f35adb640b5d2d77264228554e68deda54456eb11", + "sha256:fb02e4257376ae25c6dd95a5aec377f9b18c09be6ebdefa7ad209b9137b73d48" ], - "markers": "python_version >= '3.6'", - "version": "==2023.8.8" + "markers": "python_version >= '3.7'", + "version": "==2023.10.3" }, "rope": { "hashes": [ @@ -1401,12 +1422,12 @@ }, "tblib": { "hashes": [ - "sha256:9100bfa016b047d5b980d66e7efed952fbd20bd85b56110aaf473cb97d18709a", - "sha256:a6df30f272c08bf8be66e0775fad862005d950a6b8449b94f7c788731d70ecd7" + "sha256:80a6c77e59b55e83911e1e607c649836a69c103963c5f28a46cbeef44acf8129", + "sha256:93622790a0a29e04f0346458face1e144dc4d32f493714c6c3dff82a4adb77e6" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==2.0.0" + "markers": "python_version >= '3.8'", + "version": "==3.0.0" }, "toml": { "hashes": [ diff --git a/backend/pennmobile/settings/base.py b/backend/pennmobile/settings/base.py index 58f6bb39..eed4c350 100644 --- a/backend/pennmobile/settings/base.py +++ b/backend/pennmobile/settings/base.py @@ -49,6 +49,8 @@ "gsr_booking", "portal", "options.apps.OptionsConfig", + "sublet", + "phonenumber_field", ] MIDDLEWARE = [ diff --git a/backend/pennmobile/urls.py b/backend/pennmobile/urls.py index 208d0325..de467337 100644 --- a/backend/pennmobile/urls.py +++ b/backend/pennmobile/urls.py @@ -28,6 +28,7 @@ ), path("dining/", include("dining.urls")), path("penndata/", include("penndata.urls")), + path("sublet/", include("sublet.urls")), ] urlpatterns = [ diff --git a/backend/sublet/__init__.py b/backend/sublet/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/sublet/admin.py b/backend/sublet/admin.py new file mode 100644 index 00000000..0dbdd521 --- /dev/null +++ b/backend/sublet/admin.py @@ -0,0 +1,19 @@ +from django.contrib import admin +from django.utils.html import mark_safe + +from sublet.models import Amenity, Offer, Sublet, SubletImage + + +class SubletAdmin(admin.ModelAdmin): + def image_tag(self, instance): + images = ['' for image in instance.images.all()] + return mark_safe("
".join(images)) + + image_tag.short_description = "Sublet Images" + readonly_fields = ("image_tag",) + + +admin.site.register(Offer) +admin.site.register(Amenity) +admin.site.register(Sublet, SubletAdmin) +admin.site.register(SubletImage) diff --git a/backend/sublet/apps.py b/backend/sublet/apps.py new file mode 100644 index 00000000..9af35183 --- /dev/null +++ b/backend/sublet/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class SublettingConfig(AppConfig): + name = "sublet" + verbose_name = "Subletting" diff --git a/backend/sublet/migrations/0001_initial.py b/backend/sublet/migrations/0001_initial.py new file mode 100644 index 00000000..43426833 --- /dev/null +++ b/backend/sublet/migrations/0001_initial.py @@ -0,0 +1,129 @@ +# Generated by Django 3.2.23 on 2023-11-12 20:33 + +import django.db.models.deletion +import phonenumber_field.modelfields +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Amenity", + fields=[("name", models.CharField(max_length=255, primary_key=True, serialize=False)),], + ), + migrations.CreateModel( + name="Offer", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("email", models.EmailField(blank=True, max_length=255, null=True)), + ( + "phone_number", + phonenumber_field.modelfields.PhoneNumberField( + blank=True, max_length=128, null=True, region=None + ), + ), + ("message", models.CharField(blank=True, max_length=255)), + ("created_date", models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name="Sublet", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("title", models.CharField(max_length=255)), + ("address", models.CharField(blank=True, max_length=255, null=True)), + ("beds", models.IntegerField(blank=True, null=True)), + ("baths", models.IntegerField(blank=True, null=True)), + ("description", models.TextField(blank=True, null=True)), + ("external_link", models.URLField(max_length=255)), + ("min_price", models.IntegerField()), + ("max_price", models.IntegerField()), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("expires_at", models.DateTimeField()), + ("start_date", models.DateField()), + ("end_date", models.DateField()), + ("amenities", models.ManyToManyField(blank=True, to="sublet.Amenity")), + ( + "favorites", + models.ManyToManyField( + blank=True, related_name="sublets_favorited", to=settings.AUTH_USER_MODEL + ), + ), + ( + "sublettees", + models.ManyToManyField( + blank=True, + related_name="sublets_offered", + through="sublet.Offer", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "subletter", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), + ), + ], + ), + migrations.CreateModel( + name="SubletImage", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("image", models.ImageField(upload_to="sublet/images")), + ( + "sublet", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="images", + to="sublet.sublet", + ), + ), + ], + ), + migrations.AddField( + model_name="offer", + name="sublet", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="offers", + to="sublet.sublet", + ), + ), + migrations.AddField( + model_name="offer", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="offers_made", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddConstraint( + model_name="offer", + constraint=models.UniqueConstraint(fields=("user", "sublet"), name="unique_offer"), + ), + ] diff --git a/backend/sublet/migrations/__init__.py b/backend/sublet/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/sublet/models.py b/backend/sublet/models.py new file mode 100644 index 00000000..4fd79bd1 --- /dev/null +++ b/backend/sublet/models.py @@ -0,0 +1,58 @@ +from django.contrib.auth import get_user_model +from django.db import models +from phonenumber_field.modelfields import PhoneNumberField + + +User = get_user_model() + + +class Offer(models.Model): + class Meta: + constraints = [models.UniqueConstraint(fields=["user", "sublet"], name="unique_offer")] + + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="offers_made") + sublet = models.ForeignKey("Sublet", on_delete=models.CASCADE, related_name="offers") + email = models.EmailField(max_length=255, null=True, blank=True) + phone_number = PhoneNumberField(null=True, blank=True) + message = models.CharField(max_length=255, blank=True) + created_date = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"Offer for {self.sublet} made by {self.user}" + + +class Amenity(models.Model): + name = models.CharField(max_length=255, primary_key=True) + + def __str__(self): + return self.name + + +class Sublet(models.Model): + subletter = models.ForeignKey(User, on_delete=models.CASCADE) + sublettees = models.ManyToManyField( + User, through=Offer, related_name="sublets_offered", blank=True + ) + favorites = models.ManyToManyField(User, related_name="sublets_favorited", blank=True) + amenities = models.ManyToManyField(Amenity, blank=True) + + title = models.CharField(max_length=255) + address = models.CharField(max_length=255, null=True, blank=True) + beds = models.IntegerField(null=True, blank=True) + baths = models.IntegerField(null=True, blank=True) + description = models.TextField(null=True, blank=True) + external_link = models.URLField(max_length=255) + min_price = models.IntegerField() + max_price = models.IntegerField() + created_at = models.DateTimeField(auto_now_add=True) + expires_at = models.DateTimeField() + start_date = models.DateField() + end_date = models.DateField() + + def __str__(self): + return f"{self.title} by {self.subletter}" + + +class SubletImage(models.Model): + sublet = models.ForeignKey(Sublet, on_delete=models.CASCADE, related_name="images") + image = models.ImageField(upload_to="sublet/images") diff --git a/backend/sublet/permissions.py b/backend/sublet/permissions.py new file mode 100644 index 00000000..b90d7786 --- /dev/null +++ b/backend/sublet/permissions.py @@ -0,0 +1,44 @@ +from rest_framework import permissions + + +class IsSuperUser(permissions.BasePermission): + """ + Grants permission if the current user is a superuser. + """ + + def has_object_permission(self, request, view, obj): + return request.user.is_superuser + + def has_permission(self, request, view): + return request.user.is_superuser + + +class SubletOwnerPermission(permissions.BasePermission): + """ + Custom permission to allow the owner of a Sublet to edit or delete it. + """ + + def has_permission(self, request, view): + return request.user.is_authenticated + + def has_object_permission(self, request, view, obj): + # Check if the user is the owner of the Sublet. + if request.method in permissions.SAFE_METHODS: + return True + return obj.subletter == request.user + + +class OfferOwnerPermission(permissions.BasePermission): + """ + Custom permission to allow owner of an offer to delete it. + """ + + def has_permission(self, request, view): + return request.user.is_authenticated + + def has_object_permission(self, request, view, obj): + if request.method in permissions.SAFE_METHODS: + # Check if the user owns the sublet when getting list + return obj.subletter == request.user + # This is redundant, here for safety + return obj.user == request.user diff --git a/backend/sublet/serializers.py b/backend/sublet/serializers.py new file mode 100644 index 00000000..560e8fd1 --- /dev/null +++ b/backend/sublet/serializers.py @@ -0,0 +1,134 @@ +from phonenumber_field.serializerfields import PhoneNumberField +from rest_framework import serializers + +from sublet.models import Amenity, Offer, Sublet, SubletImage + + +class AmenitySerializer(serializers.ModelSerializer): + class Meta: + model = Amenity + fields = "__all__" + + +class OfferSerializer(serializers.ModelSerializer): + phone_number = PhoneNumberField() + + class Meta: + model = Offer + fields = "__all__" + read_only_fields = ["id", "created_date", "user"] + + def create(self, validated_data): + validated_data["user"] = self.context["request"].user + return super().create(validated_data) + + +# Create/Update Image Serializer +class SubletImageSerializer(serializers.ModelSerializer): + image = serializers.ImageField(write_only=True, required=False, allow_null=True) + + class Meta: + model = SubletImage + fields = ["sublet", "image"] + read_only_fields = ["sublet", "image"] + + +# Browse images +class SubletImageURLSerializer(serializers.ModelSerializer): + image_url = serializers.SerializerMethodField("get_image_url") + + def get_image_url(self, obj): + image = obj.image + + if not image: + return None + if image.url.startswith("http"): + return image.url + elif "request" in self.context: + return self.context["request"].build_absolute_uri(image.url) + else: + return image.url + + class Meta: + model = SubletImage + fields = ["image_url"] + + +# complex sublet serializer for use in C/U/D + getting info about a singular sublet +class SubletSerializer(serializers.ModelSerializer): + amenities = AmenitySerializer(many=True, required=False) + + class Meta: + model = Sublet + exclude = ["favorites"] + read_only_fields = ["id", "created_at", "subletter", "sublettees"] + + def parse_amenities(self, raw_amenities): + if isinstance(raw_amenities, list): + ids = raw_amenities + else: + ids = ( + list() if len(raw_amenities) == 0 else [str(id) for id in raw_amenities.split(",")] + ) + return Amenity.objects.filter(name__in=ids) + + def create(self, validated_data): + validated_data["subletter"] = self.context["request"].user + instance = super().create(validated_data) + data = self.context["request"].POST + amenities = self.parse_amenities(data.getlist("amenities")) + instance.amenities.set(amenities) + instance.save() + return instance + + def update(self, instance, validated_data): + # Check if the user is the subletter before allowing the update + if ( + self.context["request"].user == instance.subletter + or self.context["request"].user.is_superuser + ): + amenities_data = self.context["request"].data + if amenities_data.get("amenities") is not None: + amenities = self.parse_amenities(amenities_data.getlist("amenities")) + instance.amenities.set(amenities) + validated_data.pop("amenities", None) + instance = super().update(instance, validated_data) + instance.save() + else: + raise serializers.ValidationError("You do not have permission to update this sublet.") + + return instance + + def destroy(self, instance): + # Check if the user is the subletter before allowing the delete + if ( + self.context["request"].user == instance.subletter + or self.context["request"].user.is_superuser + ): + instance.delete() + else: + raise serializers.ValidationError("You do not have permission to delete this sublet.") + + +# simple sublet serializer for use when pulling all serializers/etc +class SimpleSubletSerializer(serializers.ModelSerializer): + amenities = AmenitySerializer(many=True, required=False) + images = SubletImageURLSerializer(many=True, required=False) + + class Meta: + model = Sublet + fields = [ + "id", + "subletter", + "amenities", + "title", + "address", + "beds", + "baths", + "min_price", + "max_price", + "start_date", + "end_date", + "images", + ] + read_only_fields = ["id", "subletter"] diff --git a/backend/sublet/urls.py b/backend/sublet/urls.py new file mode 100644 index 00000000..cefcf73c --- /dev/null +++ b/backend/sublet/urls.py @@ -0,0 +1,26 @@ +from django.urls import path +from rest_framework import routers + +from sublet.views import Amenities, Favorites, Offers, Properties, UserFavorites, UserOffers + + +app_name = "sublet" + +router = routers.DefaultRouter() +router.register(r"properties", Properties, basename="properties") + +additional_urls = [ + path("amenities/", Amenities.as_view(), name="amenities"), + path("favorites/", UserFavorites.as_view(), name="user-favorites"), + path("offers/", UserOffers.as_view(), name="user-offers"), + path( + "properties//favorites/", + Favorites.as_view({"post": "create", "delete": "destroy"}), + ), + path( + "properties//offers/", + Offers.as_view({"get": "list", "post": "create", "delete": "destroy"}), + ), +] + +urlpatterns = router.urls + additional_urls diff --git a/backend/sublet/views.py b/backend/sublet/views.py new file mode 100644 index 00000000..d83b72ae --- /dev/null +++ b/backend/sublet/views.py @@ -0,0 +1,207 @@ +from django.contrib.auth import get_user_model +from django.utils import timezone +from rest_framework import exceptions, generics, mixins, status, viewsets +from rest_framework.generics import get_object_or_404 +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from sublet.models import Amenity, Offer, Sublet +from sublet.permissions import IsSuperUser, OfferOwnerPermission, SubletOwnerPermission +from sublet.serializers import ( + AmenitySerializer, + OfferSerializer, + SimpleSubletSerializer, + SubletSerializer, +) + + +User = get_user_model() + + +class Amenities(generics.ListAPIView): + serializer_class = AmenitySerializer + queryset = Amenity.objects.all() + + def get(self, request, *args, **kwargs): + temp = super().get(self, request, *args, **kwargs).data + response_data = [a["name"] for a in temp] + return Response(response_data) + + +class UserFavorites(generics.ListAPIView): + serializer_class = SimpleSubletSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + user = self.request.user + return user.sublets_favorited + + +class UserOffers(generics.ListAPIView): + serializer_class = OfferSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + user = self.request.user + return Offer.objects.filter(user=user) + + +class Properties(viewsets.ModelViewSet): + """ + list: + Returns a list of Sublets that match query parameters (e.g., amenities) and belong to the user. + + create: + Create a Sublet. + + partial_update: + Update certain fields in the Sublet. Only the owner can edit it. + + destroy: + Delete a Sublet. + """ + + permission_classes = [SubletOwnerPermission | IsSuperUser] + serializer_class = SubletSerializer + + def get_queryset(self): + return Sublet.objects.all() + + # This is currently redundant but will leave for use when implementing image creation + # def create(self, request, *args, **kwargs): + # # amenities = request.data.pop("amenities", []) + # new_data = request.data + # amenities = new_data.pop("amenities", []) + + # # check if valid amenities + # try: + # amenities = [Amenity.objects.get(name=amenity) for amenity in amenities] + # except Amenity.DoesNotExist: + # return Response({"amenities": "Invalid amenity"}, status=status.HTTP_400_BAD_REQUEST) + + # serializer = self.get_serializer(data=new_data) + # serializer.is_valid(raise_exception=True) + # sublet = serializer.save() + # sublet.amenities.set(amenities) + # sublet.save() + # return Response(serializer.data, status=status.HTTP_201_CREATED) + + def list(self, request, *args, **kwargs): + """Returns a list of Sublets that match query parameters and user ownership.""" + # Get query parameters from request (e.g., amenities, user_owned) + params = request.query_params + amenities = params.getlist("amenities") + title = params.get("title") + address = params.get("address") + subletter = params.get("subletter", "false") # Defaults to False if not specified + starts_before = params.get("starts_before", None) + starts_after = params.get("starts_after", None) + ends_before = params.get("ends_before", None) + ends_after = params.get("ends_after", None) + min_price = params.get("min_price", None) + max_price = params.get("max_price", None) + beds = params.get("beds", None) + baths = params.get("baths", None) + + queryset = Sublet.objects.all().filter(expires_at__gte=timezone.now()) + + # Apply filters based on query parameters + if title: + queryset = queryset.filter(title__icontains=title) + if address: + queryset = queryset.filter(address__icontains=address) + if amenities: + queryset = queryset.filter(amenities__name__in=amenities) + if subletter.lower() == "true": + queryset = queryset.filter(subletter=request.user) + if starts_before: + queryset = queryset.filter(start_date__lt=starts_before) + if starts_after: + queryset = queryset.filter(start_date__gt=starts_after) + if ends_before: + queryset = queryset.filter(end_date__lt=ends_before) + if ends_after: + queryset = queryset.filter(end_date__gt=ends_after) + if min_price: + queryset = queryset.filter(min_price__gte=min_price) + if max_price: + queryset = queryset.filter(max_price__lte=max_price) + if beds: + queryset = queryset.filter(beds=beds) + if baths: + queryset = queryset.filter(baths=baths) + + # Serialize and return the queryset + serializer = SimpleSubletSerializer(queryset, many=True) + return Response(serializer.data) + + +class Favorites(mixins.DestroyModelMixin, mixins.CreateModelMixin, viewsets.GenericViewSet): + serializer_class = SubletSerializer + http_method_names = ["post", "delete"] + permission_classes = [IsAuthenticated | IsSuperUser] + + def get_queryset(self): + user = self.request.user + return user.sublets_favorited + + def create(self, request, *args, **kwargs): + sublet_id = int(self.kwargs["sublet_id"]) + queryset = self.get_queryset() + if queryset.filter(id=sublet_id).exists(): + raise exceptions.NotAcceptable("Favorite already exists") + sublet = get_object_or_404(Sublet, id=sublet_id) + self.get_queryset().add(sublet) + return Response(status=status.HTTP_201_CREATED) + + def destroy(self, request, *args, **kwargs): + queryset = self.get_queryset() + sublet = get_object_or_404(queryset, pk=int(self.kwargs["sublet_id"])) + self.get_queryset().remove(sublet) + return Response(status=status.HTTP_204_NO_CONTENT) + + +class Offers(viewsets.ModelViewSet): + """ + list: + Returns a list of all offers for the sublet matching the provided ID. + + create: + Create an offer on the sublet matching the provided ID. + + destroy: + Delete the offer between the user and the sublet matching the ID. + """ + + permission_classes = [OfferOwnerPermission | IsSuperUser] + serializer_class = OfferSerializer + + def get_queryset(self): + return Offer.objects.filter(sublet_id=int(self.kwargs["sublet_id"])).order_by( + "created_date" + ) + + def create(self, request, *args, **kwargs): + data = request.data + request.POST._mutable = True + if self.get_queryset().filter(user=self.request.user).exists(): + raise exceptions.NotAcceptable("Offer already exists") + data["sublet"] = int(self.kwargs["sublet_id"]) + data["user"] = self.request.user.id + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def destroy(self, request, *args, **kwargs): + queryset = self.get_queryset() + filter = {"user": self.request.user.id, "sublet": int(self.kwargs["sublet_id"])} + obj = get_object_or_404(queryset, **filter) + # checking permissions here is kind of redundant + self.check_object_permissions(self.request, obj) + self.perform_destroy(obj) + return Response(status=status.HTTP_204_NO_CONTENT) + + def list(self, request, *args, **kwargs): + self.check_object_permissions(request, Sublet.objects.get(pk=int(self.kwargs["sublet_id"]))) + return super().list(request, *args, **kwargs) diff --git a/backend/tests/sublet/__init__.py b/backend/tests/sublet/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/tests/sublet/mock_sublets.json b/backend/tests/sublet/mock_sublets.json new file mode 100644 index 00000000..d890016c --- /dev/null +++ b/backend/tests/sublet/mock_sublets.json @@ -0,0 +1,28 @@ +[ + { + "title": "Sublet1", + "address": "3465 Sansom Street", + "beds": 10, + "baths": 2, + "description": "Test sublet 1", + "external_link": "https://pennlabs.org/", + "min_price": 10, + "max_price": 500, + "expires_at": "3000-02-01T10:48:02-05:00", + "start_date": "3000-04-09", + "end_date": "3000-08-07" + }, + { + "title": "Sublet2", + "address": "1234 Broad Street", + "beds": 2, + "baths": 1, + "description": "This is test sublet2!!!", + "external_link": "https://www.google.com/maps", + "min_price": 0, + "max_price": 1000000, + "expires_at": "2999-11-28T10:49:10-05:00", + "start_date": "3000-12-19", + "end_date": "3001-03-07" + } +] \ No newline at end of file diff --git a/backend/tests/sublet/test_permissions.py b/backend/tests/sublet/test_permissions.py new file mode 100644 index 00000000..39a26c5b --- /dev/null +++ b/backend/tests/sublet/test_permissions.py @@ -0,0 +1,127 @@ +import json + +from django.contrib.auth import get_user_model +from django.test import TestCase +from rest_framework.test import APIClient + +from sublet.models import Amenity, Sublet + + +User = get_user_model() + + +class SubletPermissions(TestCase): + # TODO: Include amenities in auth test when this is done + pass + + +class OfferPermissions(TestCase): + def setUp(self): + self.client = APIClient() + self.admin = User.objects.create_superuser("admin", "admin@example.com", "admin") + self.user1 = User.objects.create_user("user1", "user1@seas.upenn.edu", "user1") + self.user2 = User.objects.create_user("user2", "user2@seas.upenn.edu", "user2") + for i in range(1, 6): + Amenity.objects.create(name=f"Amenity{str(i)}") + # TODO: Add amenities + with open("tests/sublet/mock_sublets.json") as data: + data = json.load(data) + self.sublet1 = Sublet.objects.create(subletter=self.admin, **data[0]) + self.sublet2 = Sublet.objects.create(subletter=self.user1, **data[0]) + self.sublet3 = Sublet.objects.create(subletter=self.user2, **data[1]) + + def test_authentication(self): + prop_url = f"/sublet/properties/{str(self.sublet1.id)}/offers/" + self.assertEqual(self.client.get(prop_url).status_code, 403) + self.assertEqual(self.client.post(prop_url).status_code, 403) + self.assertEqual(self.client.delete(prop_url).status_code, 403) + self.assertEqual(self.client.get("/sublet/offers/").status_code, 403) + + def create_create_offer(self): + prop_url = f"/sublet/properties/{str(self.sublet1.id)}/offers/" + payload = { + "email": "offer@seas.upenn.edu", + # This is the MERT number, please DO NOT call ;-; + "phone_number": "+12155733333", + "message": "Message", + } + users = [self.admin, self.user1] + for u in users: + self.client.force_authenticate(user=u) + self.assertEqual(self.client.post(prop_url, payload).status_code, 201) + + def test_delete_offer(self): + prop_url = f"/sublet/properties/{str(self.sublet1.id)}/offers/" + payload = { + "email": "offer@seas.upenn.edu", + # This is the MERT number, please DO NOT call ;-; + "phone_number": "+12155733333", + "message": "Message", + } + users = [self.admin, self.user1] + for u in users: + self.client.force_authenticate(user=u) + self.client.post(prop_url, payload) + self.assertEqual(self.client.delete(prop_url).status_code, 204) + + def test_get_offers_property(self): + prop_url = f"/sublet/properties/{str(self.sublet2.id)}/offers/" + payload = { + "email": "offer@seas.upenn.edu", + # This is the MERT number, please DO NOT call ;-; + "phone_number": "+12155733333", + "message": "Message", + } + users = [self.admin, self.user1, self.user2] + codes = [200, 200, 403] + for u in users: + self.client.force_authenticate(user=u) + self.client.post(prop_url, payload) + for u, c in zip(users, codes): + self.client.force_authenticate(user=u) + self.assertEqual(self.client.get(prop_url).status_code, c) + + def test_get_offers_user(self): + self.client.force_authenticate(user=self.user1) + self.assertEqual(self.client.get("/sublet/offers/").status_code, 200) + + +class FavoritePermissions(TestCase): + def setUp(self): + self.client = APIClient() + self.admin = User.objects.create_superuser("admin", "admin@example.com", "admin") + self.user1 = User.objects.create_user("user1", "user1@seas.upenn.edu", "user1") + self.user2 = User.objects.create_user("user2", "user2@seas.upenn.edu", "user2") + for i in range(1, 6): + Amenity.objects.create(name=f"Amenity{str(i)}") + # TODO: Add amenities + with open("tests/sublet/mock_sublets.json") as data: + data = json.load(data) + self.sublet1 = Sublet.objects.create(subletter=self.admin, **data[0]) + self.sublet2 = Sublet.objects.create(subletter=self.user1, **data[0]) + self.sublet3 = Sublet.objects.create(subletter=self.user2, **data[1]) + + def test_authentication(self): + prop_url = f"/sublet/properties/{str(self.sublet1.id)}/favorites/" + self.assertEqual(self.client.post(prop_url).status_code, 403) + self.assertEqual(self.client.delete(prop_url).status_code, 403) + self.assertEqual(self.client.get("/sublet/favorites/").status_code, 403) + + def test_create_favorite(self): + prop_url = f"/sublet/properties/{str(self.sublet1.id)}/favorites/" + users = [self.admin, self.user1] + for u in users: + self.client.force_authenticate(user=u) + self.assertEqual(self.client.post(prop_url).status_code, 201) + + def test_delete_favorite(self): + prop_url = f"/sublet/properties/{str(self.sublet1.id)}/favorites/" + users = [self.admin, self.user1] + for u in users: + self.client.force_authenticate(user=u) + self.client.post(prop_url) + self.assertEqual(self.client.delete(prop_url).status_code, 204) + + def test_get_favorites_user(self): + self.client.force_authenticate(user=self.user1) + self.assertEqual(self.client.get("/sublet/favorites/").status_code, 200) diff --git a/backend/tests/sublet/test_sublets.py b/backend/tests/sublet/test_sublets.py new file mode 100644 index 00000000..3229abc8 --- /dev/null +++ b/backend/tests/sublet/test_sublets.py @@ -0,0 +1,398 @@ +import json + +from django.contrib.auth import get_user_model +from django.test import TestCase +from rest_framework.test import APIClient + +from sublet.models import Amenity, Offer, Sublet + + +# , SubletImage) + + +User = get_user_model() + + +class TestSublets(TestCase): + """Tests Create/Update/Retrieve/List for sublets""" + + def setUp(self): + self.user = User.objects.create_user("user", "user@seas.upenn.edu", "user") + self.client = APIClient() + self.client.force_authenticate(user=self.user) + test_user = User.objects.create_user("user1", "user1@seas.upenn.edu", "user1") + for i in range(1, 6): + Amenity.objects.create(name=f"Amenity{str(i)}") + with open("tests/sublet/mock_sublets.json") as data: + data = json.load(data) + self.test_sublet1 = Sublet.objects.create(subletter=self.user, **data[0]) + self.test_sublet2 = Sublet.objects.create(subletter=test_user, **data[1]) + + def test_create_sublet(self): + # Create a new sublet using the serializer + payload = { + "title": "Test Sublet1", + "address": "1234 Test Street", + "beds": 2, + "baths": 1, + "description": "This is a test sublet.", + "external_link": "https://example.com", + "min_price": 100, + "max_price": 500, + "expires_at": "2024-02-01T10:48:02-05:00", + "start_date": "2024-04-09", + "end_date": "2024-08-07", + "amenities": ["Amenity1", "Amenity2"], + } + + response = self.client.post("/sublet/properties/", payload) + res_json = json.loads(response.content) + self.assertEqual(payload["beds"], res_json["beds"]) + self.assertEqual(payload["title"], res_json["title"]) + self.assertIn("created_at", res_json) + + def test_update_sublet(self): + # Create a sublet to be updated + payload = { + "title": "Test Sublet2", + "address": "1234 Old Street", + "beds": 2, + "baths": 1, + "description": "This is an old sublet.", + "external_link": "https://example.com", + "min_price": 100, + "max_price": 500, + "expires_at": "2024-02-01T10:48:02-05:00", + "start_date": "2024-04-09", + "end_date": "2024-08-07", + "amenities": ["Amenity1", "Amenity2"], + } + response = self.client.post("/sublet/properties/", payload) + old_id = json.loads(response.content)["id"] + # Update the sublet using the serializer + data = {"title": "New Title", "beds": 3, "amenities": ["Amenity1"]} + response = self.client.patch(f"/sublet/properties/{str(old_id)}/", data) + res_json = json.loads(response.content) + self.assertEqual(3, res_json["beds"]) + self.assertEqual(old_id, Sublet.objects.all().last().id) + self.assertEqual("New Title", Sublet.objects.get(id=old_id).title) + self.assertEqual("New Title", res_json["title"]) + self.assertEqual(1, len(res_json["amenities"])) + + def test_browse_sublets(self): + response = self.client.get("/sublet/properties/") + res_json = json.loads(response.content) + first_length = len(res_json) + payload = { + "title": "Test Sublet1", + "address": "1234 Test Street", + "beds": 2, + "baths": 1, + "description": "This is a test sublet.", + "external_link": "https://example.com", + "min_price": 100, + "max_price": 500, + "expires_at": "2024-02-01T10:48:02-05:00", + "start_date": "2024-04-09", + "end_date": "2024-08-07", + "amenities": ["Amenity1", "Amenity2"], + } + response = self.client.post("/sublet/properties/", payload) + old_id = json.loads(response.content)["id"] + response = self.client.get("/sublet/properties/") + res_json = json.loads(response.content) + self.assertEqual(1 + first_length, len(res_json)) + sublet = Sublet.objects.get(id=old_id) + self.assertEqual(sublet.title, "Test Sublet1") + self.assertEqual(sublet.address, "1234 Test Street") + self.assertEqual(sublet.beds, 2) + self.assertEqual(sublet.baths, 1) + + def test_browse_filtered(self): + payload = { + "title": "Test Sublet2", + "address": "1234 Test Street", + "beds": 2, + "baths": 1, + "description": "This is a test sublet.", + "external_link": "https://example.com", + "min_price": 100, + "max_price": 400, + "expires_at": "2024-02-01T10:48:02-05:00", + "start_date": "2024-04-09", + "end_date": "2024-08-07", + "amenities": ["Amenity1", "Amenity2"], + } + response = self.client.post("/sublet/properties/", payload) + old_id = json.loads(response.content)["id"] + payload = { + "title": "Sublet2", + "max_price": 450, + } + response = self.client.get("/sublet/properties/", payload) + res_json = json.loads(response.content) + sublet = res_json[0] + self.assertEqual(1, len(res_json)) + self.assertEqual(old_id, sublet["id"]) + self.assertEqual("1234 Test Street", sublet["address"]) + self.assertEqual("Test Sublet2", sublet["title"]) + response = self.client.get("/sublet/properties/", {"ends_before": "2025-05-01"}) + old_length = len(json.loads(response.content)) + payload = { + "title": "Test Sublet1", + "address": "1234 Test Street", + "beds": 2, + "baths": 1, + "description": "This is a test sublet.", + "external_link": "https://example.com", + "min_price": 100, + "max_price": 500, + "expires_at": "2024-02-01T10:48:02-05:00", + "start_date": "2024-04-09", + "end_date": "5000-08-07", + "amenities": ["Amenity1", "Amenity2"], + } + self.client.post("/sublet/properties/", payload) + response = self.client.get("/sublet/properties/", {"ends_before": "2025-05-01"}) + res_json = json.loads(response.content) + self.assertEqual(old_length, len(res_json)) + + def test_browse_sublet(self): + # browse single sublet by id + payload = { + "title": "Test Sublet2", + "address": "1234 Test Street", + "beds": 2, + "baths": 1, + "description": "This is a test sublet.", + "external_link": "https://example.com", + "min_price": 100, + "max_price": 500, + "expires_at": "2024-02-01T10:48:02-05:00", + "start_date": "2024-04-09", + "end_date": "2024-08-07", + "amenities": ["Amenity1", "Amenity2"], + } + self.client.post("/sublet/properties/", payload) + test_sublet = Sublet.objects.get(subletter=self.user, title="Test Sublet2") + response = self.client.get(f"/sublet/properties/{str(test_sublet.id)}/") + res_json = json.loads(response.content) + self.assertEqual(res_json["title"], "Test Sublet2") + self.assertEqual(res_json["address"], "1234 Test Street") + self.assertEqual(res_json["beds"], 2) + self.assertEqual(res_json["baths"], 1) + + def test_delete_sublet(self): + sublets_count = Sublet.objects.all().count() + self.client.delete(f"/sublet/properties/{str(self.test_sublet1.id)}/") + self.assertEqual(sublets_count - 1, Sublet.objects.all().count()) + self.assertFalse(Sublet.objects.filter(id=1).exists()) + + def test_amenities(self): + response = self.client.get("/sublet/amenities/") + res_json = json.loads(response.content) + for i in range(1, 6): + self.assertIn(f"Amenity{i}", res_json) + + +class TestOffers(TestCase): + """Tests Create/Delete/List for offers""" + + def setUp(self): + self.user = User.objects.create_user("user", "user@seas.upenn.edu", "user") + self.client = APIClient() + self.client.force_authenticate(user=self.user) + self.test_user = User.objects.create_user("user1", "user") + for i in range(1, 6): + Amenity.objects.create(name=f"Amenity{str(i)}") + # TODO: Not sure how to add these amenities to the sublets, but not important for now + with open("tests/sublet/mock_sublets.json") as data: + data = json.load(data) + self.first_sublet = Sublet.objects.create(subletter=self.user, **data[0]) + self.second_sublet = Sublet.objects.create(subletter=self.test_user, **data[1]) + + def test_create_offer(self): + prop_url = f"/sublet/properties/{str(self.second_sublet.id)}/offers/" + payload = { + "email": "offer@seas.upenn.edu", + # This is the MERT number, please DO NOT call ;-; + "phone_number": "+12155733333", + "message": "Message", + } + self.client.post(prop_url, payload) + self.assertEqual(self.client.post(prop_url, payload).status_code, 406) + offer = Offer.objects.get(pk=1) + offer_list = [offer.email, offer.phone_number, offer.message, offer.user, offer.sublet] + payload_list = [ + payload["email"], + payload["phone_number"], + payload["message"], + self.user, + self.second_sublet, + ] + for o, p in zip(offer_list, payload_list): + self.assertEqual(o, p) + self.assertIsNotNone(offer.id) + self.assertIsNotNone(offer.created_date) + + def test_delete_offer(self): + prop_url1 = f"/sublet/properties/{str(self.first_sublet.id)}/offers/" + prop_url2 = f"/sublet/properties/{str(self.second_sublet.id)}/offers/" + payload = { + "email": "offer@seas.upenn.edu", + # This is the MERT number, please DO NOT call ;-; + "phone_number": "+12155733333", + "message": "Message", + } + self.client.post(prop_url2, payload) + offers_count = Offer.objects.all().count() + self.assertEqual(self.client.delete(prop_url1).status_code, 404) + offers_count_new = Offer.objects.all().count() + self.assertEqual(offers_count, offers_count_new) + self.client.delete(prop_url2) + self.assertFalse(Offer.objects.filter(user=self.user, sublet=self.second_sublet).exists()) + + def test_get_offers_property(self): + response = self.client.get("/sublet/offers/") + res_json = json.loads(response.content) + self.assertEqual(0, len(res_json)) + payload = { + "email": "offer@seas.upenn.edu", + # This is the MERT number, please DO NOT call ;-; + "phone_number": "+12155733333", + "message": "Message", + } + self.client.post(f"/sublet/properties/{str(self.first_sublet.id)}/offers/", payload) + response = self.client.get(f"/sublet/properties/{str(self.first_sublet.id)}/offers/") + self.assertEqual(1, len(json.loads(response.content))) + Offer.objects.create( + user=self.test_user, + sublet=self.first_sublet, + email="offer2@seas.upenn.edu", + phone_number="+12155733334", + message="Message2", + ) + response = self.client.get(f"/sublet/properties/{str(self.first_sublet.id)}/offers/") + res_json = json.loads(response.content) + self.assertEqual(2, len(res_json)) + # TODO: this is really ugly, maybe clean up later haha + offer = res_json[0] + self.assertEqual(offer["email"], "offer@seas.upenn.edu") + # This is the MERT number, please DO NOT call ;-; + self.assertEqual(offer["phone_number"], "+12155733333") + self.assertEqual(offer["message"], "Message") + self.assertEqual(offer["user"], self.user.id) + self.assertEqual(offer["sublet"], self.first_sublet.id) + self.assertIsNotNone(offer["id"]) + self.assertIsNotNone(offer["created_date"]) + offer = res_json[1] + self.assertEqual(offer["email"], "offer2@seas.upenn.edu") + self.assertEqual(offer["phone_number"], "+12155733334") + self.assertEqual(offer["message"], "Message2") + self.assertEqual(offer["user"], self.test_user.id) + self.assertEqual(offer["sublet"], self.first_sublet.id) + self.assertIsNotNone(offer["id"]) + self.assertIsNotNone(offer["created_date"]) + + def test_get_offer_user(self): + response = self.client.get("/sublet/offers/") + res_json = json.loads(response.content) + self.assertEqual(0, len(res_json)) + payload = { + "email": "offer@seas.upenn.edu", + # This is the MERT number, please DO NOT call ;-; + "phone_number": "+12155733333", + "message": "Message", + } + self.client.post(f"/sublet/properties/{str(self.first_sublet.id)}/offers/", payload) + response = self.client.get("/sublet/offers/") + self.assertEqual(1, len(json.loads(response.content))) + payload = { + "email": "offer2@seas.upenn.edu", + "phone_number": "+12155733334", + "message": "Message2", + } + self.client.post(f"/sublet/properties/{str(self.second_sublet.id)}/offers/", payload) + response = self.client.get("/sublet/offers/") + res_json = json.loads(response.content) + self.assertEqual(2, len(res_json)) + offer = res_json[0] + self.assertEqual(offer["email"], "offer@seas.upenn.edu") + # This is the MERT number, please DO NOT call ;-; + self.assertEqual(offer["phone_number"], "+12155733333") + self.assertEqual(offer["message"], "Message") + self.assertEqual(offer["user"], self.user.id) + self.assertEqual(offer["sublet"], self.first_sublet.id) + self.assertIsNotNone(offer["id"]) + self.assertIsNotNone(offer["created_date"]) + offer = res_json[1] + self.assertEqual(offer["email"], "offer2@seas.upenn.edu") + self.assertEqual(offer["phone_number"], "+12155733334") + self.assertEqual(offer["message"], "Message2") + self.assertEqual(offer["user"], self.user.id) + self.assertEqual(offer["sublet"], self.second_sublet.id) + self.assertIsNotNone(offer["id"]) + self.assertIsNotNone(offer["created_date"]) + + +class TestFavorites(TestCase): + """Tests Create/Delete/List for favorites""" + + def setUp(self): + self.user = User.objects.create_user("user", "user@seas.upenn.edu", "user") + self.client = APIClient() + self.client.force_authenticate(user=self.user) + test_user = User.objects.create_user("user1", "user") + for i in range(1, 6): + Amenity.objects.create(name=f"Amenity{str(i)}") + # TODO: Not sure how to add these amenities to the sublets, but not important for now + with open("tests/sublet/mock_sublets.json") as data: + data = json.load(data) + self.first_sublet = Sublet.objects.create(subletter=self.user, **data[0]) + self.second_sublet = Sublet.objects.create(subletter=test_user, **data[1]) + + def test_create_favorite(self): + prop_url1 = f"/sublet/properties/{str(self.first_sublet.id)}/favorites/" + prop_url2 = f"/sublet/properties/{str(self.second_sublet.id)}/favorites/" + self.client.post(prop_url2) + self.assertTrue(self.user.sublets_favorited.filter(pk=self.second_sublet.id).exists()) + self.assertFalse(self.user.sublets_favorited.filter(pk=self.first_sublet.id).exists()) + self.client.post(prop_url1) + self.assertTrue(self.user.sublets_favorited.filter(pk=self.second_sublet.id).exists()) + self.assertTrue(self.user.sublets_favorited.filter(pk=self.first_sublet.id).exists()) + self.assertEqual(self.client.post(prop_url1).status_code, 406) + + def test_delete_favorite(self): + self.client.post(f"/sublet/properties/{str(self.second_sublet.id)}/favorites/") + self.client.post(f"/sublet/properties/{str(self.first_sublet.id)}/favorites/") + self.client.delete(f"/sublet/properties/{str(self.first_sublet.id)}/favorites/") + self.assertTrue(self.user.sublets_favorited.filter(pk=self.second_sublet.id).exists()) + self.assertFalse(self.user.sublets_favorited.filter(pk=self.first_sublet.id).exists()) + self.client.post(f"/sublet/properties/{str(self.first_sublet.id)}/favorites/") + self.assertTrue(self.user.sublets_favorited.filter(pk=self.second_sublet.id).exists()) + self.assertTrue(self.user.sublets_favorited.filter(pk=self.first_sublet.id).exists()) + self.client.delete(f"/sublet/properties/{str(self.first_sublet.id)}/favorites/") + self.assertTrue(self.user.sublets_favorited.filter(pk=self.second_sublet.id).exists()) + self.assertFalse(self.user.sublets_favorited.filter(pk=self.first_sublet.id).exists()) + self.client.delete(f"/sublet/properties/{str(self.second_sublet.id)}/favorites/") + self.assertFalse(self.user.sublets_favorited.filter(pk=self.second_sublet.id).exists()) + self.assertFalse(self.user.sublets_favorited.filter(pk=self.first_sublet.id).exists()) + + def test_get_favorite_user(self): + response = self.client.get("/sublet/favorites/") + res_json = json.loads(response.content) + self.assertEqual(len(res_json), 0) + self.client.post(f"/sublet/properties/{str(self.second_sublet.id)}/favorites/") + response = self.client.get("/sublet/favorites/") + res_json = json.loads(response.content) + self.assertEqual(len(res_json), 1) + self.assertEqual(res_json[0]["id"], self.second_sublet.id) + self.client.post(f"/sublet/properties/{str(self.first_sublet.id)}/favorites/") + response = self.client.get("/sublet/favorites/") + res_json = json.loads(response.content) + self.assertEqual(len(res_json), 2) + self.client.delete(f"/sublet/properties/{str(self.second_sublet.id)}/favorites/") + response = self.client.get("/sublet/favorites/") + res_json = json.loads(response.content) + self.assertEqual(len(res_json), 1) + self.assertEqual(res_json[0]["id"], self.first_sublet.id)