diff --git a/.flake8 b/.flake8 index e15622d..68faef7 100644 --- a/.flake8 +++ b/.flake8 @@ -4,4 +4,9 @@ exclude = *migrations* ./config/settings.py ./team_production_system/management/commands/add_superuser.py - +dictionaries = en_US,python,technical,django +; The bugbear plugin allows for a 10% safety net, so max-line-length is effectively 88 +; This is due to ignoring E501 and adding B950 +max-line-length = 80 +extend-ignore = E501 +extend-select = B950 \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 97aad03..f2508c2 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,10 +2,9 @@ ### **1. Targeted Issue** ### -*(The first step to a good solution is knowing the problem. Please link the issue here.)* +*(The first step to a good solution is knowing the problem. Please link the issue here. Make sure to say 'fixes' before the linked issue)* -Issue #... or -Ticket #... +This PR fixes Issue #... ### **2. Overview of Solution** ### diff --git a/.gitignore b/.gitignore index e972286..0f5b560 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,8 @@ local_settings.py db.sqlite3 db.sqlite3-journal media +requirements.txt +media/profile_photo/* # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ # in your Git repository. Update and uncomment the following line accordingly. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 941e6c3..aef88e3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,14 +1,47 @@ repos: +- repo: https://github.com/PyCQA/isort + rev: "5.12.0" + hooks: + - id: isort + stages: [pre-commit] +- repo: https://github.com/psf/black + rev: "23.10.1" + hooks: + - id: black + stages: [pre-commit] +- repo: https://github.com/PyCQA/flake8 + rev: "6.1.0" + hooks: + - id: flake8 + stages: [pre-commit] + additional_dependencies: + - "flake8-bugbear" + - "pep8-naming" + - "flake8-spellcheck" + - "flake8-eradicate" + - "flake8-multiline-containers" + - "flake8-clean-block" + - "flake8-secure-coding-standard" + - "flake8-comprehensions" + - "flake8-quotes" - repo: local hooks: + - id: commit-msg-lint + name: 'Commit Message Lint' + entry: ./scripts/commit-msg-lint.sh + language: script + stages: [commit-msg] - id: branch-name-lint name: 'Branch Name Lint' entry: ./scripts/branch-name-lint.sh language: script always_run: true pass_filenames: false - - id: commit-msg-lint - name: 'Commit Message Lint' - entry: ./scripts/commit-msg-lint.sh - language: script - stages: [commit-msg] \ No newline at end of file + stages: [pre-push] + - id: django-test + name: 'Checking Tests' + entry: pipenv run python manage.py test + always_run: true + pass_filenames: false + language: system + stages: [pre-push] diff --git a/Pipfile b/Pipfile index 32541f9..a154929 100644 --- a/Pipfile +++ b/Pipfile @@ -22,15 +22,25 @@ django-multiselectfield = "*" boto3 = "*" sentry-sdk = {extras = ["django"], version = "*"} pytz = "*" -flake8 = "*" redis = "*" -coverage = "*" celery = "*" django-celery-beat = "*" -pre-commit = "*" [dev-packages] -autopep8 = "*" +flake8 = "*" +flake8-bugbear = "*" +pep8-naming = "*" +flake8-spellcheck = "*" +flake8-eradicate = "*" +black = "*" +flake8-multiline-containers = "*" +flake8-clean-block = "*" +flake8-secure-coding-standard = "*" +isort = "*" +flake8-comprehensions = "*" +flake8-quotes = "*" +pre-commit = "*" +coverage = "*" [requires] python_version = "3.10" diff --git a/Pipfile.lock b/Pipfile.lock index f0e2aad..8bbc16c 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "db5550df8f89bfd52f3e07ec791eb1e004f3821342dcbf6ca0a9b9652184a25a" + "sha256": "1cdd5af9b4bb6cc3add05bae0c649184807370a7e8fcadfc85a1d80eafcb4243" }, "pipfile-spec": 6, "requires": { @@ -18,11 +18,11 @@ "default": { "amqp": { "hashes": [ - "sha256:2c1b13fecc0893e946c65cbd5f36427861cffa4ea2201d8f6fca22e2a373b5e2", - "sha256:6f0956d2c23d8fa6e7691934d8c3930eadb44972cbbd1a7ae3a520f735d43359" + "sha256:827cb12fb0baa892aad844fd95258143bce4027fdac4fccddbc43330fd281637", + "sha256:a1ecff425ad063ad42a486c902807d1482311481c8ad95a72694b2975e75f7fd" ], "markers": "python_version >= '3.6'", - "version": "==5.1.1" + "version": "==5.2.0" }, "asgiref": { "hashes": [ @@ -42,37 +42,37 @@ }, "billiard": { "hashes": [ - "sha256:0f50d6be051c6b2b75bfbc8bfd85af195c5739c281d3f5b86a5640c65563614a", - "sha256:1ad2eeae8e28053d729ba3373d34d9d6e210f6e4d8bf0a9c64f92bd053f1edf5" + "sha256:07aa978b308f334ff8282bd4a746e681b3513db5c9a514cbdd810cbbdc19714d", + "sha256:9a3c3184cb275aa17a732f93f65b20c525d3d9f253722d26a82194803ade5a2c" ], "markers": "python_version >= '3.7'", - "version": "==4.1.0" + "version": "==4.2.0" }, "boto3": { "hashes": [ - "sha256:5ddf24cf52c7fb6aaa332eaa08ae8c2afc8f2d1e8860680728533dd573904e32", - "sha256:e2d2824ba6459b330d097e94039a9c4f96ae3f4bcdc731d620589ad79dcd16d3" + "sha256:6617ac176efb21485ebc3a058a3a97feb1300141421ae3d1809562c4cac1d5f9", + "sha256:f3024bba9ac980007ba7b5f28a9734d111fb5466e2426ac76c5edbd6dedd8db2" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==1.28.57" + "version": "==1.29.2" }, "botocore": { "hashes": [ - "sha256:002f8bdca8efde50ae7267f342bc1d03a71d76024ce3949e4ffdd1151581c53e", - "sha256:83a3ca4d9247fdbde76c654137e6ab648bd976f652ce2354def1715c838af505" + "sha256:0e231524e9b72169fe0b8d9310f47072c245fb712778e0669f53f264f0e49536", + "sha256:a68a33193d8cd59e3b2142bff632e562afc02f9c4417e3dcc81a6e1b1f47148e" ], "markers": "python_version >= '3.7'", - "version": "==1.31.58" + "version": "==1.32.2" }, "celery": { "hashes": [ - "sha256:1e6ed40af72695464ce98ca2c201ad0ef8fd192246f6c9eac8bba343b980ad34", - "sha256:9023df6a8962da79eb30c0c84d5f4863d9793a466354cc931d7f72423996de28" + "sha256:30b75ac60fb081c2d9f8881382c148ed7c9052031a75a1e8743ff4b4b071f184", + "sha256:6b65d8dd5db499dd6190c45aa6398e171b99592f2af62c312f7391587feb5458" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==5.3.4" + "version": "==5.3.5" }, "certifi": { "hashes": [ @@ -140,109 +140,101 @@ "markers": "python_version >= '3.8'", "version": "==1.16.0" }, - "cfgv": { - "hashes": [ - "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", - "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560" - ], - "markers": "python_version >= '3.8'", - "version": "==3.4.0" - }, "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:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", + "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", + "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", + "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", + "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", + "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", + "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", + "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", + "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", + "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", + "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", + "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", + "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", + "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", + "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", + "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", + "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", + "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", + "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", + "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", + "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", + "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", + "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", + "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", + "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", + "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", + "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", + "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", + "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", + "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", + "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", + "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", + "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", + "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", + "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", + "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", + "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", + "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", + "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", + "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", + "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", + "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", + "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", + "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", + "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", + "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", + "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", + "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", + "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", + "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", + "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", + "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", + "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", + "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", + "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", + "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", + "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", + "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", + "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", + "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", + "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", + "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", + "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", + "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", + "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", + "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", + "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", + "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", + "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", + "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", + "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", + "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", + "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", + "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", + "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" ], "markers": "python_full_version >= '3.7.0'", - "version": "==3.3.0" + "version": "==3.3.2" }, "click": { "hashes": [ @@ -275,65 +267,6 @@ "markers": "python_version >= '3.6'", "version": "==0.3.0" }, - "coverage": { - "hashes": [ - "sha256:025ded371f1ca280c035d91b43252adbb04d2aea4c7105252d3cbc227f03b375", - "sha256:04312b036580ec505f2b77cbbdfb15137d5efdfade09156961f5277149f5e344", - "sha256:0575c37e207bb9b98b6cf72fdaaa18ac909fb3d153083400c2d48e2e6d28bd8e", - "sha256:07d156269718670d00a3b06db2288b48527fc5f36859425ff7cec07c6b367745", - "sha256:1f111a7d85658ea52ffad7084088277135ec5f368457275fc57f11cebb15607f", - "sha256:220eb51f5fb38dfdb7e5d54284ca4d0cd70ddac047d750111a68ab1798945194", - "sha256:229c0dd2ccf956bf5aeede7e3131ca48b65beacde2029f0361b54bf93d36f45a", - "sha256:245c5a99254e83875c7fed8b8b2536f040997a9b76ac4c1da5bff398c06e860f", - "sha256:2829c65c8faaf55b868ed7af3c7477b76b1c6ebeee99a28f59a2cb5907a45760", - "sha256:4aba512a15a3e1e4fdbfed2f5392ec221434a614cc68100ca99dcad7af29f3f8", - "sha256:4c96dd7798d83b960afc6c1feb9e5af537fc4908852ef025600374ff1a017392", - "sha256:50dd1e2dd13dbbd856ffef69196781edff26c800a74f070d3b3e3389cab2600d", - "sha256:5289490dd1c3bb86de4730a92261ae66ea8d44b79ed3cc26464f4c2cde581fbc", - "sha256:53669b79f3d599da95a0afbef039ac0fadbb236532feb042c534fbb81b1a4e40", - "sha256:553d7094cb27db58ea91332e8b5681bac107e7242c23f7629ab1316ee73c4981", - "sha256:586649ada7cf139445da386ab6f8ef00e6172f11a939fc3b2b7e7c9082052fa0", - "sha256:5ae4c6da8b3d123500f9525b50bf0168023313963e0e2e814badf9000dd6ef92", - "sha256:5b4ee7080878077af0afa7238df1b967f00dc10763f6e1b66f5cced4abebb0a3", - "sha256:5d991e13ad2ed3aced177f524e4d670f304c8233edad3210e02c465351f785a0", - "sha256:614f1f98b84eb256e4f35e726bfe5ca82349f8dfa576faabf8a49ca09e630086", - "sha256:636a8ac0b044cfeccae76a36f3b18264edcc810a76a49884b96dd744613ec0b7", - "sha256:6407424621f40205bbe6325686417e5e552f6b2dba3535dd1f90afc88a61d465", - "sha256:6bc6f3f4692d806831c136c5acad5ccedd0262aa44c087c46b7101c77e139140", - "sha256:6cb7fe1581deb67b782c153136541e20901aa312ceedaf1467dcb35255787952", - "sha256:74bb470399dc1989b535cb41f5ca7ab2af561e40def22d7e188e0a445e7639e3", - "sha256:75c8f0df9dfd8ff745bccff75867d63ef336e57cc22b2908ee725cc552689ec8", - "sha256:770f143980cc16eb601ccfd571846e89a5fe4c03b4193f2e485268f224ab602f", - "sha256:7eb0b188f30e41ddd659a529e385470aa6782f3b412f860ce22b2491c89b8593", - "sha256:7eb3cd48d54b9bd0e73026dedce44773214064be93611deab0b6a43158c3d5a0", - "sha256:87d38444efffd5b056fcc026c1e8d862191881143c3aa80bb11fcf9dca9ae204", - "sha256:8a07b692129b8a14ad7a37941a3029c291254feb7a4237f245cfae2de78de037", - "sha256:966f10df9b2b2115da87f50f6a248e313c72a668248be1b9060ce935c871f276", - "sha256:a6191b3a6ad3e09b6cfd75b45c6aeeffe7e3b0ad46b268345d159b8df8d835f9", - "sha256:aab8e9464c00da5cb9c536150b7fbcd8850d376d1151741dd0d16dfe1ba4fd26", - "sha256:ac3c5b7e75acac31e490b7851595212ed951889918d398b7afa12736c85e13ce", - "sha256:ac9ad38204887349853d7c313f53a7b1c210ce138c73859e925bc4e5d8fc18e7", - "sha256:b9c0c19f70d30219113b18fe07e372b244fb2a773d4afde29d5a2f7930765136", - "sha256:c397c70cd20f6df7d2a52283857af622d5f23300c4ca8e5bd8c7a543825baa5a", - "sha256:c6601a60318f9c3945be6ea0f2a80571f4299b6801716f8a6e4846892737ebe4", - "sha256:c6f55d38818ca9596dc9019eae19a47410d5322408140d9a0076001a3dcb938c", - "sha256:ca70466ca3a17460e8fc9cea7123c8cbef5ada4be3140a1ef8f7b63f2f37108f", - "sha256:ca833941ec701fda15414be400c3259479bfde7ae6d806b69e63b3dc423b1832", - "sha256:cd0f7429ecfd1ff597389907045ff209c8fdb5b013d38cfa7c60728cb484b6e3", - "sha256:cd694e19c031733e446c8024dedd12a00cda87e1c10bd7b8539a87963685e969", - "sha256:cdd088c00c39a27cfa5329349cc763a48761fdc785879220d54eb785c8a38520", - "sha256:de30c1aa80f30af0f6b2058a91505ea6e36d6535d437520067f525f7df123887", - "sha256:defbbb51121189722420a208957e26e49809feafca6afeef325df66c39c4fdb3", - "sha256:f09195dda68d94a53123883de75bb97b0e35f5f6f9f3aa5bf6e496da718f0cb6", - "sha256:f12d8b11a54f32688b165fd1a788c408f927b0960984b899be7e4c190ae758f1", - "sha256:f1a317fdf5c122ad642db8a97964733ab7c3cf6009e1a8ae8821089993f175ff", - "sha256:f2781fd3cabc28278dc982a352f50c81c09a1a500cc2086dc4249853ea96b981", - "sha256:f4f456590eefb6e1b3c9ea6328c1e9fa0f1006e7481179d749b3376fc793478e" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==7.3.1" - }, "cron-descriptor": { "hashes": [ "sha256:b6ff4e3a988d7ca04a4ab150248e9f166fb7a5c828a85090e75bcc25aa93b4dd" @@ -342,32 +275,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" }, "defusedxml": { "hashes": [ @@ -377,21 +310,14 @@ "markers": "python_version >= '3.6'", "version": "==0.8.0rc2" }, - "distlib": { - "hashes": [ - "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057", - "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8" - ], - "version": "==0.3.7" - }, "django": { "hashes": [ - "sha256:5e5c1c9548ffb7796b4a8a4782e9a2e5a3df3615259fc1bfd3ebc73b646146c1", - "sha256:b6b2b5cae821077f137dc4dade696a1c2aa292f892eca28fa8d7bfdf2608ddd4" + "sha256:8e0f1c2c2786b5c0e39fe1afce24c926040fad47c8ea8ad30aaf1188df29fc41", + "sha256:e1d37c51ad26186de355cbcec16613ebdabfa9689bbade9c538835205a8abbe9" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.2.5" + "version": "==4.2.7" }, "django-appconf": { "hashes": [ @@ -411,12 +337,12 @@ }, "django-cors-headers": { "hashes": [ - "sha256:25aabc94d4837678c1edf442c7f68a5f5fd151f6767b0e0b01c61a2179d02711", - "sha256:bd36c7aea0d070e462f3383f0dc9ef717e5fdc2b10a99c98c285f16da84ffba2" + "sha256:0b1fd19297e37417fc9f835d39e45c8c642938ddba1acce0c1753d3edef04f36", + "sha256:0bf65ef45e606aff1994d35503e6b677c0b26cafff6506f8fd7187f3be840207" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.2.0" + "version": "==4.3.1" }, "django-environ": { "hashes": [ @@ -458,17 +384,17 @@ "sha256:20c7c5c449e33eed5fd45ef8d3dc668faabaeff3277eddd1892b262d686ba381" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==7.1.0" + "markers": "python_version >= '3.8'", + "version": "==7.2.0" }, "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" }, "django-templated-mail": { "hashes": [ @@ -504,29 +430,12 @@ }, "djoser": { "hashes": [ - "sha256:4aa48502df870c8b5f07109ad4a749cc881c37bb5efa85cf5462ea695a0dca8c", - "sha256:7b24718cdc51b4294b0abcf6bf0ead11aa3ca83652e351dfb04b7b8b15afa3b0" + "sha256:9deb831a1c8781ceff325699e1407b4e1be8b4588e87071621d88ba31c09349f", + "sha256:efb91ad61e4d5b8d664db029b5947df9d34078289ef2680a1ab665e047144b74" ], "index": "pypi", "markers": "python_version >= '3.8' and python_version < '4.0'", - "version": "==2.2.0" - }, - "filelock": { - "hashes": [ - "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4", - "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd" - ], - "markers": "python_version >= '3.8'", - "version": "==3.12.4" - }, - "flake8": { - "hashes": [ - "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23", - "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5" - ], - "index": "pypi", - "markers": "python_full_version >= '3.8.1'", - "version": "==6.1.0" + "version": "==2.2.2" }, "gunicorn": { "hashes": [ @@ -537,14 +446,6 @@ "markers": "python_version >= '3.5'", "version": "==21.2.0" }, - "identify": { - "hashes": [ - "sha256:afe67f26ae29bab007ec21b03d4114f41316ab9dd15aa8736a167481e108da54", - "sha256:f302a4256a15c849b91cfcdcec052a8ce914634b2f77ae87dad29cd749f2d88d" - ], - "markers": "python_version >= '3.8'", - "version": "==2.5.30" - }, "idna": { "hashes": [ "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", @@ -563,27 +464,11 @@ }, "kombu": { "hashes": [ - "sha256:0ba213f630a2cb2772728aef56ac6883dc3a2f13435e10048f6e97d48506dbbd", - "sha256:b753c9cfc9b1e976e637a7cbc1a65d446a22e45546cd996ea28f932082b7dc9e" + "sha256:0bb2e278644d11dea6272c17974a3dbb9688a949f3bb60aeb5b791329c44fadc", + "sha256:63bb093fc9bb80cfb3a0972336a5cec1fa7ac5f9ef7e8237c6bf8dda9469313e" ], "markers": "python_version >= '3.8'", - "version": "==5.3.2" - }, - "mccabe": { - "hashes": [ - "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", - "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" - ], - "markers": "python_version >= '3.6'", - "version": "==0.7.0" - }, - "nodeenv": { - "hashes": [ - "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2", - "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", - "version": "==1.8.0" + "version": "==5.3.4" }, "oauthlib": { "hashes": [ @@ -603,11 +488,11 @@ }, "phonenumbers": { "hashes": [ - "sha256:001664c90f59b8954766c2db85adafc8dbc96177efeb49607ca4e64a7acaf569", - "sha256:85ceeba9e67984ba98182c77e8e4c70093d38c0c6a0cb2bd392e0694ddaeb1f6" + "sha256:4ae2d2e253a4752a269ae1147822b9aa500f14b2506a91f884e68b136901f128", + "sha256:7a57cceb8145d3099a0cda7a1f2581b6829936069224790be5de0adf14b39f13" ], "index": "pypi", - "version": "==8.13.22" + "version": "==8.13.25" }, "pilkit": { "hashes": [ @@ -618,164 +503,151 @@ }, "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" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==10.0.1" - }, - "platformdirs": { - "hashes": [ - "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3", - "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e" - ], - "markers": "python_version >= '3.7'", - "version": "==3.11.0" - }, - "pre-commit": { - "hashes": [ - "sha256:6bbd5129a64cad4c0dfaeeb12cd8f7ea7e15b77028d985341478c8af3c759522", - "sha256:96d529a951f8b677f730a7212442027e8ba53f9b04d217c4c67dc56c393ad945" + "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": "==3.4.0" + "version": "==10.1.0" }, "prompt-toolkit": { "hashes": [ - "sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac", - "sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88" + "sha256:941367d97fc815548822aa26c2a269fdc4eb21e9ec05fc5d447cf09bad5d75f0", + "sha256:f36fe301fafb7470e86aaf90f036eef600a3210be4decf461a5b1ca8403d3cb2" ], "markers": "python_full_version >= '3.7.0'", - "version": "==3.0.39" + "version": "==3.0.41" }, "psycopg2-binary": { "hashes": [ - "sha256:01f9731761f711e42459f87bd2ad5d744b9773b5dd05446f3b579a0f077e78e3", - "sha256:0e3071c947bda6afc6fe2e7b64ebd64fb2cad1bc0e705a3594cb499291f2dfec", - "sha256:14f85ff2d5d826a7ce9e6c31e803281ed5a096789f47f52cb728c88f488de01b", - "sha256:15458c81b0d199ab55825007115f697722831656e6477a427783fe75c201c82b", - "sha256:19d40993701e39c49b50e75cd690a6af796d7e7210941ee0fe49cf12b25840e5", - "sha256:1d669887df169a9b0c09e0f5b46891511850a9ddfcde3593408af9d9774c5c3a", - "sha256:1dbad789ebd1e61201256a19dc2e90fed4706bc966ccad4f374648e5336b1ab4", - "sha256:1f279ba74f0d6b374526e5976c626d2ac3b8333b6a7b08755c513f4d380d3add", - "sha256:205cecdd81ff4f1ddd687ce7d06879b9b80cccc428d8d6ebf36fcba08bb6d361", - "sha256:278ebd63ced5a5f3af5394cb75a9a067243eee21f42f0126c6f1cf85eaeb90f9", - "sha256:3723c3f009e2b2771f2491b330edb7091846f1aad0c08fbbd9a1383d6a0c0841", - "sha256:395c217156723fe21809dfe8f7a433c5bf8e9bce229944668e4ec709c37c5442", - "sha256:3ae22a0fa5c516b84ddb189157fabfa3f12eded5d630e1ce260a18e1771f8707", - "sha256:3b6928a502af71ca2ac9aad535e78c8309892ed3bfa7933182d4c760580c8af4", - "sha256:3b6c607ecb6a9c245ebe162d63ccd9222d38efa3c858bbe38d32810b08b8f87e", - "sha256:3fd44b52bc9c74c1512662e8da113a1c55127adeeacebaf460babe766517b049", - "sha256:4336afc0e81726350bd5863e3c3116d8c12aa7f457d3d0b3b3dc36137fec6feb", - "sha256:4879ee1d07a6b2c232ae6a74570f4788cd7a29b3cd38bc39bf60225b1d075c78", - "sha256:4960c881471ca710b81a67ef148c33ee121c1f8e47a639cf7e06537fe9fee337", - "sha256:4b8b2cdf3bce4dd91dc035fbff4eb812f5607dda91364dc216b0920b97b521c7", - "sha256:4bfabbd7e70785af726cc0209e8e64b926abf91741eca80678b221aad9e72135", - "sha256:4d6b592ecc8667e608b9e7344259fbfb428cc053df0062ec3ac75d8270cd5a9f", - "sha256:5262713988d97a9d4cd54b682dec4a413b87b76790e5b16f480450550d11a8f7", - "sha256:54bf5c27bd5867a5fa5341fad29f0d5838e2fed617ef5346884baf8b8b16dd82", - "sha256:565edaf9f691b17a7fdbabd368b5b3e67d0fdc8f7f6b52177c1d3289f4e763fd", - "sha256:59421806c1a0803ea7de9ed061d656c041a84db0da7e73266b98db4c7ba263da", - "sha256:59f45cca0765aabb52a5822c72d5ff2ec46a28b1c1702de90dc0d306ec5c2001", - "sha256:5a0a6e4004697ec98035ff3b8dfc4dba8daa477b23ee891d831cd3cd65ace6be", - "sha256:5aa0c99c12075c593dcdccbb8a7aaa714b716560cc99ef9206f9e75b77520801", - "sha256:5aef3296d44d05805e634dbbd2972aa8eb7497926dd86047f5e39a79c3ecc086", - "sha256:5cbe1e19f59950afd66764e3c905ecee9f2aee9f8df2ef35af6f7948ad93f620", - "sha256:5debcb23a052f3fb4c165789ea513b562b2fac0f0f4f53eaf3cf4dc648907ff8", - "sha256:5f955fe6301b84b6fd13970a05f3640fbb62ca3a0d19342356585006c830e038", - "sha256:6369f4bd4d27944498094dccced1ae7ca43376a59dbfe4c8b6a16e9e3dc3ccce", - "sha256:63ce1dccfd08d9c5341ac82d62aa04345bc4bf41b5e5b7b2c6c172a28e0eda27", - "sha256:65403113ac3a4813a1409fb6a1e43c658b459cc8ed8afcc5f4baf02ec8be4334", - "sha256:673eafbdaa4ed9f5164c90e191c3895cc5f866b9b379fdb59f3a2294e914d9bd", - "sha256:693a4e7641556f0b421a7d6c6a74058aead407d860ac1cb9d0bf25be0ca73de8", - "sha256:6e1bb4eb0d9925d65dabaaabcbb279fab444ba66d73f86d4c07dfd11f0139c06", - "sha256:6f5e70e40dae47a4dc7f8eb390753bb599b0f4ede314580e6faa3b7383695d19", - "sha256:80451e6b6b7c486828d5c7ed50769532bbb04ec3a411f1e833539d5c10eb691c", - "sha256:8c84ff9682bc4520504c474e189b3de7c4a4029e529c8b775e39c95c33073767", - "sha256:91719f53ed2a95ebecefac48d855d811cba9d9fe300acc162993bdfde9bc1c3b", - "sha256:9a971086db0069aef2fd22ccffb670baac427f4ee2174c4f5c7206254f1e6794", - "sha256:aeb09db95f38e75ae04e947d283e07be34d03c4c2ace4f0b73dbb9143d506e67", - "sha256:c68a2e1afb4f2a5bb4b7bb8f90298d21196ac1c66418523e549430b8c4b7cb1e", - "sha256:c7ff2b6a79a92b1b169b03bb91b41806843f0cdf6055256554495bffed1d496d", - "sha256:ccaa2ae03990cedde1f618ff11ec89fefa84622da73091a67b44553ca8be6711", - "sha256:cf60c599c40c266a01c458e9c71db7132b11760f98f08233f19b3e0a2153cbf1", - "sha256:d29efab3c5d6d978115855a0f2643e0ee8c6450dc536d5b4afec6f52ab99e99e", - "sha256:d4a19a3332f2ac6d093e60a6f1c589f97eb9f9de7e27ea80d67f188384e31572", - "sha256:dc145a241e1f6381efb924bcf3e3462d6020b8a147363f9111eb0a9c89331ad7", - "sha256:de85105c568dc5f0f0efe793209ba83e4675d53d00faffc7a7c7a8bea9e0e19a", - "sha256:e11373d8e4f1f46cf3065bf613f0df9854803dc95aa4a35354ffac19f8c52127", - "sha256:e271ad6692d50d70ca75db3bd461bfc26316de78de8fe1f504ef16dcea8f2312", - "sha256:e3142c7e51b92855cff300580de949e36a94ab3bfa8f353b27fe26535e9b3542", - "sha256:e46b0f4683539965ce849f2c13fc53e323bb08d84d4ba2e4b3d976f364c84210", - "sha256:e6ef615d48fa60361e57f998327046bd89679c25d06eee9e78156be5a7a76e03", - "sha256:e7bdc94217ae20ad03b375a991e107a31814053bee900ad8c967bf82ef3ff02e", - "sha256:fc37de7e3a87f5966965fc874d33c9b68d638e6c3718fdf32a5083de563428b0" + "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9", + "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77", + "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e", + "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84", + "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3", + "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2", + "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67", + "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876", + "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152", + "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f", + "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a", + "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6", + "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503", + "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f", + "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493", + "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996", + "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f", + "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e", + "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59", + "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94", + "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7", + "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682", + "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420", + "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae", + "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291", + "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe", + "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980", + "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93", + "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692", + "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119", + "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716", + "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472", + "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b", + "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2", + "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc", + "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c", + "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5", + "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab", + "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984", + "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9", + "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf", + "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0", + "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f", + "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212", + "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb", + "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be", + "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90", + "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041", + "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7", + "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860", + "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d", + "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245", + "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27", + "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417", + "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359", + "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202", + "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0", + "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7", + "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba", + "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1", + "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd", + "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07", + "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98", + "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55", + "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d", + "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972", + "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f", + "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e", + "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26", + "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957", + "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53", + "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52" ], "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==2.9.8" - }, - "pycodestyle": { - "hashes": [ - "sha256:259bcc17857d8a8b3b4a2327324b79e5f020a13c16074670f9c8c8f872ea76d0", - "sha256:5d1013ba8dc7895b548be5afb05740ca82454fd899971563d2ef625d090326f8" - ], - "markers": "python_version >= '3.8'", - "version": "==2.11.0" + "markers": "python_version >= '3.7'", + "version": "==2.9.9" }, "pycparser": { "hashes": [ @@ -784,14 +656,6 @@ ], "version": "==2.21" }, - "pyflakes": { - "hashes": [ - "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774", - "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc" - ], - "markers": "python_version >= '3.8'", - "version": "==3.1.0" - }, "pyjwt": { "hashes": [ "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de", @@ -812,7 +676,7 @@ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.8.2" }, "python3-openid": { @@ -830,62 +694,6 @@ "index": "pypi", "version": "==2023.3.post1" }, - "pyyaml": { - "hashes": [ - "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", - "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", - "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", - "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", - "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", - "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", - "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", - "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", - "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", - "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", - "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", - "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", - "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", - "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", - "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", - "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", - "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", - "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", - "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", - "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", - "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", - "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", - "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", - "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", - "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", - "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", - "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", - "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", - "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", - "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", - "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", - "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", - "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", - "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", - "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", - "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", - "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", - "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", - "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", - "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", - "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", - "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", - "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", - "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", - "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", - "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", - "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", - "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", - "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", - "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" - ], - "markers": "python_version >= '3.6'", - "version": "==6.0.1" - }, "redis": { "hashes": [ "sha256:0dab495cd5753069d3bc650a0dde8a8f9edde16fc5691b689a566eda58100d0f", @@ -924,42 +732,34 @@ "django" ], "hashes": [ - "sha256:64a7141005fb775b9db298a30de93e3b83e0ddd1232dc6f36eb38aebc1553291", - "sha256:6de2e88304873484207fed836388e422aeff000609b104c802749fd89d56ba5b" - ], - "version": "==1.31.0" - }, - "setuptools": { - "hashes": [ - "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87", - "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a" + "sha256:04e392db9a0d59bd49a51b9e3a92410ac5867556820465057c2ef89a38e953e9", + "sha256:a7865952701e46d38b41315c16c075367675c48d049b90a4cc2e41991ebc7efa" ], - "markers": "python_version >= '3.8'", - "version": "==68.2.2" + "version": "==1.35.0" }, "six": { "hashes": [ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "social-auth-app-django": { "hashes": [ - "sha256:2e71234656ddebe0c5b5ad450d42ee49f52a3f2d1708687fccf2a2c92d31a624", - "sha256:8719d57d01d80dcc9629a46e6806889aa9714fe4b658d2ebe3c120450591031d" + "sha256:09ac02a063cb313eed5e9ef2f9ac4477c8bf5bbd685925ff3aba43f9072f1bbb", + "sha256:28c65b2e2092f30cdb3cf912eeaa6988b49fdf4001b29bd89e683673d700a38e" ], - "markers": "python_version >= '3.7'", - "version": "==5.3.0" + "markers": "python_version >= '3.8'", + "version": "==5.4.0" }, "social-auth-core": { "hashes": [ - "sha256:9791d7c7aee2ac8517fe7a2ea2f942a8a5492b3a4ccb44a9b0dacc87d182f2aa", - "sha256:ea7a19c46b791b767e95f467881b53c5fd0d1efb40048d9ed3dbc46daa05c954" + "sha256:3d4154f45c0bacffe54ccf4361bce7e66cf5f5cd1bb0ebb7507ad09a1b07d9d9", + "sha256:f4ae5d8e503a401f319498bcad59fd1f6c473517eeae89c22299250f63c33365" ], - "markers": "python_version >= '3.6'", - "version": "==4.4.2" + "markers": "python_version >= '3.8'", + "version": "==4.5.0" }, "sqlparse": { "hashes": [ @@ -987,35 +787,26 @@ }, "urllib3": { "hashes": [ - "sha256:24d6a242c28d29af46c3fae832c36db3bbebcc533dd1bb549172cd739c82df21", - "sha256:94a757d178c9be92ef5539b8840d48dc9cf1b2709c9d6b588232a055c524458b" + "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84", + "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e" ], - "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.26.17" + "markers": "python_version >= '3.6'", + "version": "==2.0.7" }, "vine": { "hashes": [ - "sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30", - "sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e" + "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", + "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0" ], "markers": "python_version >= '3.6'", - "version": "==5.0.0" - }, - "virtualenv": { - "hashes": [ - "sha256:b80039f280f4919c77b30f1c23294ae357c4c8701042086e3fc005963e4e537b", - "sha256:e8361967f6da6fbdf1426483bfe9fca8287c242ac0bc30429905721cefbff752" - ], - "markers": "python_version >= '3.7'", - "version": "==20.24.5" + "version": "==5.1.0" }, "wcwidth": { "hashes": [ - "sha256:77f719e01648ed600dfa5402c347481c0992263b81a027344f3e1ba25493a704", - "sha256:8705c569999ffbb4f6a87c6d1b80f324bd6db952f5eb0b95bc07517f4c1813d4" + "sha256:390c7454101092a6a5e43baad8f83de615463af459201709556b6e4b1c861f97", + "sha256:aec5179002dd0f0d40c456026e74a729661c9d468e1ed64405e3a6c2176ca36f" ], - "version": "==0.2.8" + "version": "==0.2.10" }, "whitenoise": { "hashes": [ @@ -1023,27 +814,381 @@ "sha256:b1f9db9bf67dc183484d760b99f4080185633136a273a03f6436034a41064146" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==6.5.0" + "markers": "python_version >= '3.8'", + "version": "==6.6.0" } }, "develop": { - "autopep8": { + "attrs": { "hashes": [ - "sha256:067959ca4a07b24dbd5345efa8325f5f58da4298dab0dde0443d5ed765de80cb", - "sha256:2913064abd97b3419d1cc83ea71f042cb821f87e45b9c88cad5ad3c4ea87fe0c" + "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04", + "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015" + ], + "markers": "python_version >= '3.7'", + "version": "==23.1.0" + }, + "black": { + "hashes": [ + "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4", + "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b", + "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f", + "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07", + "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187", + "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6", + "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05", + "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06", + "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e", + "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5", + "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244", + "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f", + "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221", + "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055", + "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479", + "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394", + "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911", + "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142" ], "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==23.11.0" + }, + "cfgv": { + "hashes": [ + "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", + "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560" + ], + "markers": "python_version >= '3.8'", + "version": "==3.4.0" + }, + "click": { + "hashes": [ + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.7" + }, + "coverage": { + "hashes": [ + "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1", + "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63", + "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9", + "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312", + "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3", + "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb", + "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25", + "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92", + "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda", + "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148", + "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6", + "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216", + "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a", + "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640", + "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836", + "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c", + "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f", + "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2", + "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901", + "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed", + "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a", + "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074", + "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc", + "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84", + "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083", + "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f", + "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c", + "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c", + "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637", + "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2", + "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82", + "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f", + "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce", + "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef", + "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f", + "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611", + "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c", + "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76", + "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9", + "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce", + "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9", + "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf", + "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf", + "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9", + "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6", + "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2", + "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a", + "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a", + "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf", + "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738", + "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a", + "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==7.3.2" + }, + "distlib": { + "hashes": [ + "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057", + "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8" + ], + "version": "==0.3.7" + }, + "eradicate": { + "hashes": [ + "sha256:06df115be3b87d0fc1c483db22a2ebb12bcf40585722810d809cc770f5031c37", + "sha256:2b29b3dd27171f209e4ddd8204b70c02f0682ae95eecb353f10e8d72b149c63e" + ], + "version": "==2.3.0" + }, + "filelock": { + "hashes": [ + "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e", + "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c" + ], + "markers": "python_version >= '3.8'", + "version": "==3.13.1" + }, + "flake8": { + "hashes": [ + "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23", + "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5" + ], + "index": "pypi", + "markers": "python_full_version >= '3.8.1'", + "version": "==6.1.0" + }, + "flake8-bugbear": { + "hashes": [ + "sha256:90cf04b19ca02a682feb5aac67cae8de742af70538590509941ab10ae8351f71", + "sha256:b182cf96ea8f7a8595b2f87321d7d9b28728f4d9c3318012d896543d19742cb5" + ], + "index": "pypi", + "markers": "python_full_version >= '3.8.1'", + "version": "==23.9.16" + }, + "flake8-clean-block": { + "hashes": [ + "sha256:0a1f8a58be210a013715810a78dda029a65660373f585b7e14e36c3b85c7791d", + "sha256:9684c26a6b087e25f73966fe8f4a38f6abb3e9737a9f781140115e0e2c14a93c" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==0.1.2" + }, + "flake8-comprehensions": { + "hashes": [ + "sha256:7b9d07d94aa88e62099a6d1931ddf16c344d4157deedf90fe0d8ee2846f30e97", + "sha256:81768c61bfc064e1a06222df08a2580d97de10cb388694becaf987c331c6c0cf" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==3.14.0" + }, + "flake8-eradicate": { + "hashes": [ + "sha256:18acc922ad7de623f5247c7d5595da068525ec5437dd53b22ec2259b96ce9d22", + "sha256:aee636cb9ecb5594a7cd92d67ad73eb69909e5cc7bd81710cf9d00970f3983a6" + ], + "index": "pypi", + "markers": "python_version >= '3.8' and python_version < '4.0'", + "version": "==1.5.0" + }, + "flake8-multiline-containers": { + "hashes": [ + "sha256:1b684da84b401f42f1ad36f0f90e24b7f49b2691458cb582911fb99d871bb0b2", + "sha256:7c47527f1a2b0a991b876e58a2758e0ecc6b2d10a5fd4ee7740d042722f2f281" + ], + "index": "pypi", + "version": "==0.0.19" + }, + "flake8-quotes": { + "hashes": [ + "sha256:6e26892b632dacba517bf27219c459a8396dcfac0f5e8204904c5a4ba9b480e1" + ], + "index": "pypi", + "version": "==3.3.2" + }, + "flake8-secure-coding-standard": { + "hashes": [ + "sha256:0c7222b8c92f85dc38014a53d6a1a8dab4267363b128afe480cbc2d1c29b2df0", + "sha256:a0393cd3ed5bd44a79735e15d1845e9416cda5b3fe39941984e2d9714916d779" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.4.0" + }, + "flake8-spellcheck": { + "hashes": [ + "sha256:6fb79724f27097af0b4c678c7eaf731f0d87cf4c1eae7fbd7836c1c78d67e12c", + "sha256:a0a37164b9175819b143ce0f0a8d2475457af3f0d77cd8423b0daf204662ee72" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==0.28.0" + }, + "identify": { + "hashes": [ + "sha256:7736b3c7a28233637e3c36550646fc6389bedd74ae84cb788200cc8e2dd60b75", + "sha256:90199cb9e7bd3c5407a9b7e81b4abec4bb9d249991c79439ec8af740afc6293d" + ], + "markers": "python_version >= '3.8'", + "version": "==2.5.31" + }, + "isort": { + "hashes": [ + "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504", + "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6" + ], + "index": "pypi", + "markers": "python_full_version >= '3.8.0'", + "version": "==5.12.0" + }, + "mccabe": { + "hashes": [ + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" + ], "markers": "python_version >= '3.6'", - "version": "==2.0.4" + "version": "==0.7.0" + }, + "mypy-extensions": { + "hashes": [ + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.0" + }, + "nodeenv": { + "hashes": [ + "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2", + "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", + "version": "==1.8.0" + }, + "packaging": { + "hashes": [ + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2" + }, + "pathspec": { + "hashes": [ + "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20", + "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3" + ], + "markers": "python_version >= '3.7'", + "version": "==0.11.2" + }, + "pep8-naming": { + "hashes": [ + "sha256:1705f046dfcd851378aac3be1cd1551c7c1e5ff363bacad707d43007877fa971", + "sha256:1a86b8c71a03337c97181917e2b472f0f5e4ccb06844a0d6f0a33522549e7a80" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==0.13.3" + }, + "platformdirs": { + "hashes": [ + "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3", + "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e" + ], + "markers": "python_version >= '3.7'", + "version": "==3.11.0" + }, + "pre-commit": { + "hashes": [ + "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32", + "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==3.5.0" }, "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": [ + "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774", + "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc" + ], + "markers": "python_version >= '3.8'", + "version": "==3.1.0" + }, + "pyyaml": { + "hashes": [ + "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", + "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", + "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", + "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", + "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", + "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", + "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", + "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", + "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", + "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", + "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", + "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", + "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", + "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", + "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", + "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", + "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", + "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", + "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", + "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", + "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", + "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", + "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", + "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", + "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", + "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", + "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", + "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", + "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", + "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", + "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", + "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", + "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", + "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", + "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", + "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", + "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", + "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", + "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", + "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", + "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", + "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", + "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", + "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", + "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", + "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", + "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", + "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", + "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", + "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" + ], + "markers": "python_version >= '3.6'", + "version": "==6.0.1" + }, + "setuptools": { + "hashes": [ + "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87", + "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a" + ], + "markers": "python_version >= '3.8'", + "version": "==68.2.2" }, "tomli": { "hashes": [ @@ -1052,6 +1197,22 @@ ], "markers": "python_version < '3.11'", "version": "==2.0.1" + }, + "typing-extensions": { + "hashes": [ + "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0", + "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef" + ], + "markers": "python_version < '3.11'", + "version": "==4.8.0" + }, + "virtualenv": { + "hashes": [ + "sha256:02ece4f56fbf939dbbc33c0715159951d6bf14aaf5457b092e4548e1382455af", + "sha256:520d056652454c5098a00c0f073611ccbea4c79089331f60bf9d7ba247bb7381" + ], + "markers": "python_version >= '3.7'", + "version": "==20.24.6" } } } diff --git a/README.md b/README.md index 719e458..4f482b1 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,9 @@ Team Production System is an app for mentees to schedule one-on-one sessions wit - [Running Celery and Redis Locally](#run-celery-and-redis-locally) - [Run Locally via Docker Containers](#run-locally-via-docker-containers) - [Environment Variables](#environment-variables) +- [Adding Packages](#adding-packages) - [Testing](#testing) +- [Linting](#linting) - [Submitting Code](#submitting-code) - [API Reference](#api-reference) @@ -24,12 +26,14 @@ Please adhere to this project's [code of conduct](https://github.com/TeamProduct ## Features - Users can setup profiles as a mentor or mentee. -- Mentors can set their skill set and avalibilty. -- Mentees can schedule sessions with the menots, filtered by skills and avalibilty +- Mentors can set their skill set and availabilty. +- Mentees can schedule sessions with the mentors, filtered by skills and availabilty - Mentors can confirm sessions - Both mentor and mentee can cancel a session prior to session start time. -# Run Locally +# Running Locally + +## Local Setup Clone the project: @@ -61,12 +65,18 @@ Install the project dependencies: pipenv install ``` +## Running without Containers + Set up the database by running the migrations: ```bash python manage.py migrate ``` +💡**Note:** If this command throws an error, you might not have +[setup your database](#setting-up-a-postgresql-database), +or [configured your DATABASE_URL env variable properly](#environment-variables). + Start the development server: ```bash @@ -75,9 +85,10 @@ python manage.py runserver The app should now be running at http://localhost:8000/ -## Run Celery and Redis locally +### Run Celery and Redis locally -Only needed if you want 15/60 minute reminders of scheduled sessions. +These servers are only needed if you want 15/60 minute reminders of scheduled sessions. +Run each of the following commands in its own terminal window at the project root. Start the Redis server: @@ -88,37 +99,57 @@ redis-server Start the Celery server: ```bash -celery -A config.celery worker --loglevel=info +celery -A config.celery_settings worker --loglevel=info ``` Start the Celery Beat server: ```bash -celery -A config.celery beat -l debug +celery -A config.celery_settings beat -l debug ``` ## Run Locally via Docker Containers -**Note:** Docker and Docker Desktop are required to be installed on your machine for this method. -You will also need to have your .env file set up. +### Setup + +Install Docker Desktop. This will also install the Docker engine and the Docker CLI. +You can find installation instructions on the Docker website for +[Mac](https://docs.docker.com/desktop/install/mac-install/) and +[Windows](https://docs.docker.com/desktop/install/windows-install/). + +Setup your Environment Variables. You can find instructions [here](#environment-variables). + +Create or update `requirements.txt` with any new plugins from Pipfile: -Update `requirements.txt` with any newly added installs: ```bash pipenv requirements > requirements.txt ``` -**Note:** If this step deletes everything in the requirements.txt file, your pipenv is out of date. +💡**Note:** If this step deletes everything in the requirements.txt file, your pipenv is out of date. You can update it with the following command: + ```bash pip install --user --upgrade pipenv ``` -Build docker images: +### Building Docker Image + +Run the following command: + ```bash docker compose build ``` -Spin up docker containers: +If you haven't built the container before, this can take over a minute. +After that, Docker will use the cached image layers as reference for future builds. +You can expect the build to only take seconds. + +If you want to build the image from scratch, add the flag `--no-cache` to the above command. + +### Running Docker Containers + +Run the following command: + ```bash docker compose up ``` @@ -130,26 +161,42 @@ Use the DJANGO_SUPERUSER credentials you set in the .env file. If you want to connect to the container database via an app like Postico 2, the settings needed are: - - Host: localhost - - Port: 5433 - - Database: mentors - - User: mentors - - Password: mentors + - Host: localhost + - Port: 5433 + - Database: mentors + - User: mentors + - Password: mentors + +While running, the Django server will automatically detect changes made and +reload, just as if it was running in your local environment. +Certain file changes, such as to a model, won't trigger this behavior. +In these cases, stop then restart the containers. + +### Stopping Docker Containers + +To stop running the containers, hit `Ctrl+C`. The container instances will not be deleted. +If you spin them up again, those same instances will be running. + +If you want to delete the container instances, run the following command: -To stop running the containers, hit Ctrl+C, then spin down the containers: ```bash docker compose down ``` -The database is persistant. If you want to reset it, follow these 2 steps once the containers are no longer running: +The database is persistant. If you make changes to a model, run makemigrations +before resetting the the database. +Follow these 2 steps once the containers are no longer running: - Remove the persistant volume: + ```bash docker volume rm team_production_system_be_postgres_data -``` -- Rebuild the docker images: +``` + +- Rebuild the docker images without the cached data: + ```bash -docker compose build +docker compose build --no-cache ``` The next time you spin up the docker containers, the database will be empty again. @@ -163,21 +210,33 @@ The next time you spin up the docker containers, the database will be empty agai For example: ``` -DATABASE_PASSWORD=mentors -DATABASE_NAME=mentors -DATABASE_USER=mentors -DATABASE_HOST=localhost -DATABASE_PORT=5432 +ENVIRONMENT=dev +DATABASE_URL=postgres://mentors:mentors@localhost:5432/mentors SECRET_KEY=my_secret_key DEBUG=True + DJANGO_SUPERUSER_USERNAME=admin DJANGO_SUPERUSER_PASSWORD=admin_password DJANGO_SUPERUSER_EMAIL=admin@example.com -CELERY_BROKER_URL = local_redis_url -CELERY_RESULT_BACKEND = local_redis_url + +CELERY_BROKER_URL=local_redis_url +CELERY_RESULT_BACKEND=local_redis_url + +AWS_ACCESS_KEY_ID=from-your-aws-account +AWS_SECRET_ACCESS_KEY=also_from-your-aws-account +AWS_STORAGE_BUCKET_NAME=your-aws-s3-bucket + +EMAIL_HOST=usually-gmail +EMAIL_HOST_USER=example@fake.com +EMAIL_HOST_PASSWORD=do-not-share + +SENTRY_DSN=not-necessary-for-running-locally ``` -- DATABASE_URL: This should be set to the URL of your database. Depending on your database type, this may include a username, password, host, and port. +- ENVIRONMENT: This should be either `dev` or `prod`, depending on what environment the app is running in. +As long as your are running locally, use the value `dev`. + +- DATABASE_URL: This should be set to the URL of your database. Depending on your database type, this may include a username, password, host, and port. When using a local PostgreSQL database, it should take the form `postgres://:@localhost:5432/` - SECRET_KEY: This should be set to a secret key that is used for cryptographic signing in Django. It is important that this value is kept secret and is not shared publicly. @@ -193,40 +252,171 @@ CELERY_RESULT_BACKEND = local_redis_url - CELERY_RESULT_BACKEND: This should be set to your local redis url. +- AWS_ACCESS_KEY: This should be set to your AWS account. + +- AWS_SECRET_ACCESS_KEY: This should be set to your AWS account. + +- AWS_STORAGE_BUCKET_NAME: This should be set to your AWS S3 instance. + +- EMAIL_HOST: This should be set to the email account you want to use. + +- EMAIL_HOST_USER: This should be set to the email account you want to use. + +- EMAIL_HOST_PASSWORD: This should be set to the email account you want to use. + +- SENTRY_DSN: This should be set to your Sentry account. + 3. Save the .env file. + +## Adding Packages + +[Pipenv](https://pipenv.pypa.io/en/latest/) is a packaging tool for Python that solves some common problems associated with the typical development workflow using pip, virtualenv, and the requirements.txt. In addition to addressing some common issues, it consolidates and simplifies the development process to a single command line tool. + +When installing new packages, you first need to assess if they are needed for production and development or only development. + +For packages needed for both prod and dev, please run installation as follows: + +```bash +pipenv install +``` + +If the package is only needed for development, please run installation as follows: + +```bash +pipenv install --dev +``` +Providing the --dev argument will put the dependency in a special [dev-packages] location in the Pipfile. These development packages only get installed if you specify the --dev argument with pipenv install. + +If you update the Pipfile and are using Docker locally for development, please make sure to spin down your docker containers, copy over the Pipfile information to requirements.txt and then rebuild your containers. See [Run Locally via Docker Containers](#run-locally-via-docker-containers) for instructions. + + # Testing For testing this app, we are using [Django Test Case](https://docs.djangoproject.com/en/4.2/topics/testing/overview/) and [Django REST Framework API Test Case](https://www.django-rest-framework.org/api-guide/testing/#api-test-cases) along with [coverage.py](https://coverage.readthedocs.io/en/7.2.7/index.html) for test coverage reporting. To run tests: -`python manage.py test` -To skip a test that isn't finished, add the following before the test class: +```bash +python manage.py test +``` + +💡**Note:** To skip a test that isn't finished, add the following before the test class: `@unittest.skip("Test file is not ready yet")` To run coverage for test: -`coverage run manage.py test` +```bash +coverage run manage.py test +``` After you run tests you can get the report in command-line by running: -`coverage report` +```bash +coverage report +``` For an interactive html report, run: -`coverage html` +```bash +coverage html +``` Then in the `htmlcov` folder of the project, open the file `index.html` in a browser. Here you can see an indepth analysis of coverage and what lines need testing. Click available links to view specific file coverage data. Here is some helpful information on testing in Django and Django REST Framework: https://www.rootstrap.com/blog/testing-in-django-django-rest-basics-useful-tools-good-practices +# Linting + +To keep our code easy to read and use please make sure it passes flake8 linting before submitting your code. To run in terminal: + +```bash +flake8 +``` + +Each error will show the file name and line to find the error. The command can be run over and over again until errors are cleared. + # Submitting Code -We use a pre-commit to check branch names and commit messages. Please follow the the following schema for branch names and commit messages: +We use several [git hooks](https://git-scm.com/docs/githooks) to ensure code quality +and consistency. These are run using the [pre-commit](https://pre-commit.com/) +Python package. + +Here are the processes we run for each hook: +- **[Pre-Commit](https://git-scm.com/docs/githooks#_pre_commit):** + - linting code with Flake8 + - autoformatting code with isort and black +- **[Commit-Msg](https://git-scm.com/docs/githooks#_commit_msg):** + - linting commit message +- **[Pre-Push](https://git-scm.com/docs/githooks#_pre_push):** + - linting branch name + - running all tests + +Below, we go over how to set up and use these hooks, the linting plugins used, +and the schemas for Branch Names and Commit Messages. + +## Pre-Commit Setup +These steps assume you have already entered a pipenv shell and installed all +dependencies. +Below are the commands and expected outputs: + +```bash +pre-commit install +``` +`pre-commit installed at .git/hooks/pre-commit` + +```bash +pre-commit install --hook-type commit-msg +``` +`pre-commit installed at .git/hooks/commit-msg` + +```bash +pre-commit install --hook-type pre-push +``` +`pre-commit installed at .git/hooks/pre-push` -## Branch Names +To run the pre-commit checks before making a commit, run the following command. + +```bash +pre-commit run --all-files +``` + +If no files need changes, the output should look like this: + +`isort....................................................................Passed` +`black....................................................................Passed` +`flake8...................................................................Passed` + +If isort or black catch any errors, they will automatically alter the files to fix them. +This will prevent making a commit, and you will need to stage the new changes. +flake8 errors need to be fixed manually. + +## Code Linting + +We ensure code consistancy by linting with flake8. +Errors found by flake 8 will be listed in the following format: +```bash +::: +``` +Error codes from flake8 will have a prefix of F. +The flake8 plugins used are listed below, along with their error code prefix: + + - flake8-bugbear (B): additional rules to catch bugs and design problems + - pep8-naming (N): check the PEP-8 naming conventions + - flake8-spellcheck (SC): spellcheck variables, classnames, comments, docstrings etc. + - flake8-eradicate (E): finds commented out or dead code + - flake8-clean-block (CLB): enforces a blank line after if/for/while/with/try blocks + - flake8-multiline (JS): ensures a consistent format for multiline containers + - flake8-secure-coding-standard (SCS): enforces some secure coding standards for Python + - flake8-comprehensions (C): helps you write better list/set/dict comprehensions + - flake8-quotes (Q): extension for checking quotes in Python + +💡**Note:** If the spellcheck plugin gets caught on a name that you did not set, +add it to `whitelist.txt`. +**DO NOT ADD NAMES THAT YOU CREATE!!!** + +## Branch Name Schema Branch names should be in the following format: -`//` +`/issue-/` **Type:** The type of branch. This should be one of the following: @@ -240,7 +430,9 @@ Branch names should be in the following format: **Description:** A short description of the branch. This should be in lowercase and use dashes instead of spaces. -## Commit Messages +**Example:** `feat/issue-66/remove-jedi` + +## Commit Message Schema Commit messages should be in the following format: @@ -260,9 +452,38 @@ Commit messages should be in the following format: - chore - Miscellaneous changes, such as updating packages or bumping a version number - revert - Reverting a previous commit -**Scope:** This is optional but can provide additional contextual information. It describes the section or aspect of the codebase affected by the change. For example, auth for authentication-related changes or header for changes to a website's header component. +**Scope:** This is optional but can provide additional contextual information. +It describes the section or aspect of the codebase affected by the change. +For example, auth for authentication-related changes or header for changes to a website's header component. + +**Description:** A concise description of the changes. +Start with a lowercase verb indicating what was done (e.g., add, update, remove, fix). + +## Setting Up a PostgreSQL Database -**Description:** A concise description of the changes. Start with a lowercase verb indicating what was done (e.g., add, update, remove, fix). +💡**Note:** This guide assumes you are using a macOS. If you are using a different +operating system, only the installation step should be different. Here are guides +for [Windows](https://www.postgresqltutorial.com/postgresql-getting-started/install-postgresql/) +and [Linux](https://www.postgresqltutorial.com/postgresql-getting-started/install-postgresql-linux/). + +Install PostgreSQL on your machine, then run it as a background service: + +```bash +brew install postgresql@15 +brew services start postgresql@15 +``` + +Next, create a user: + +```bash +createuser -d +``` + +Then, create a database: + +```bash +createdb -U +``` # API Reference @@ -270,17 +491,19 @@ API URL - https://team-production-system.onrender.com ## Quck Links: -- [User Endpoints](#user-create) -- [Mentor Endpoints](#view-mentors-list-user-authentication-required) -- [Mentee Endpoints](#view-mentee-list-user-authentication-required) -- [Availability Endpoints](#mentors-availabilty-user-authentication-required) -- [Session Endpoints](#sessions-user-authentication-required) -- [Notification Endpoints](#update-notification-settings-user-authentication-required) +- [User Endpoints](#user-endpoints) +- [Mentor Endpoints](#mentor-endpoints) +- [Mentee Endpoints](#mentee-endpoints) +- [Availability Endpoints](#availability-endpoints) +- [Session Endpoints](#session-endpoints) +- [Notification Endpoints](#notification-endpoints) + +## User Endpoints ## User Create - Create a new user -- **Note: the username will automatically be converted to all lowercase letters** +- 💡**Note: the username will automatically be converted to all lowercase letters** ```http POST https://team-production-system.onrender.com/auth/users/ @@ -304,7 +527,7 @@ Host: https://team-production-system.onrender.com "username": "TestUserLogin", "email": "testemail@fake.com", "password": "TestUserPassword", - "re_password": "TestUserPassword", + "re_password": "TestUserPassword" } ``` @@ -325,6 +548,7 @@ Host: https://team-production-system.onrender.com ## Token Authentication / User Login - Create a user token. +- Username must be lowercase ```http POST - https://team-production-system.onrender.com/auth/token/login/ @@ -332,7 +556,7 @@ POST - https://team-production-system.onrender.com/auth/token/login/ | Body | Type | Description | | :--------- | :------- | :---------------------- | -| `username` | `string` | Username | +| `username` | `string` | Username (lowercase) | | `password` | `string` | User generated password | #### Request Sample: @@ -384,7 +608,7 @@ Host: https://team-production-system.onrender.com { "username": "testuserlogin" , - "password": "TestUserPassword", + "password": "TestUserPassword" } ``` @@ -455,7 +679,7 @@ Host: https://team-production-system.onrender.com ## Edit User Profile (User Authentication **Required**) - Update the users profile information. -- **Note: This endpoint has multipart/form-data content type.** +- 💡**Note: This endpoint has multipart/form-data content type.** ```http PATCH - https://team-production-system.onrender.com/myprofile/ @@ -517,6 +741,8 @@ Host: https://team-production-system.onrender.com --- +## Mentor Endpoints + ## View Mentors List (User Authentication **Required**) - View a list of all user with the mentors flag (Expired availabilties are filtered out) @@ -597,10 +823,18 @@ Host: https://team-production-system.onrender.com POST - https://team-production-system.onrender.com/mentorinfo/ ``` -| Body | Type | Description | -| :--------- | :------- | :------------------------- | -| `about_me` | `string` | Information about the user | -| `skills` | `string` | Skills the user has | +| Body | Type | Description | +| :------------ | :------- | :------------------------- | +| `pk` | `int` | The mentor pk | +| `about_me` | `string` | Information about the user | +| `skills` | `array` | Skills the user has | +| `team_number` | `int` | Mentor's team number | + +**Nested Information:** + +| Body | Type | Description | +| :--------------- | :------ | :---------------------------- | +| `availabilities` | `array` | Availabilities the mentor has | #### Request Sample: @@ -612,7 +846,8 @@ Host: https://team-production-system.onrender.com { "about_me": "Hi, I am so and so and do such and such", - "skills": "CSS" + "skills": ["CSS"], + "team_number": 10, } ``` @@ -621,8 +856,13 @@ Host: https://team-production-system.onrender.com ```JSON { + "pk": 1, "about_me": "Hi, I am so and so and do such and such", - "skills": "CSS" + "skills": [ + "CSS" + ], + "availabilities": [], + "team_number": 10 } ``` @@ -637,10 +877,12 @@ Host: https://team-production-system.onrender.com GET - https://team-production-system.onrender.com/mentorinfo/ ``` -| Body | Type | Description | -| :--------- | :------- | :------------------------- | -| `about_me` | `string` | Information about the user | -| `skills` | `string` | Skills the user has | +| Body | Type | Description | +| :------------ | :------- | :------------------------- | +| `pk` | `int` | The mentor pk | +| `about_me` | `string` | Information about the user | +| `skills` | `string` | Skills the user has | +| `team_number` | `int` | Mentor's team number | **Nested Information:** @@ -666,8 +908,11 @@ Host: https://team-production-system.onrender.com ```JSON { + "pk": 1, "about_me": "Hi, I am so and so and do such and such", - "skills": "CSS" + "skills": [ + "CSS" + ], "availabilities": [ { "pk": 1, @@ -675,10 +920,9 @@ Host: https://team-production-system.onrender.com "start_time": "2023-04-12T14:30:00Z", "end_time": "2023-04-12T15:30:00Z" } - ] - + ], + "team_number": 10 } - ``` --- @@ -691,10 +935,18 @@ Host: https://team-production-system.onrender.com PATCH - https://team-production-system.onrender.com/mentorinfoupdate/ ``` -| Body | Type | Description | -| :--------- | :------- | :------------------------- | -| `about_me` | `string` | Information about the user | -| `skills` | `string` | Skills the user has | +| Body | Type | Description | +| :------------ | :------- | :------------------------- | +| `pk` | `int` | The mentor pk | +| `about_me` | `string` | Information about the user | +| `skills` | `array` | Skills the user has | +| `team_number` | `int` | Mentor's team number | + +**Nested Information:** + +| Body | Type | Description | +| :--------------- | :------ | :---------------------------- | +| `availabilities` | `array` | Availabilities the mentor has | #### Request Sample: @@ -714,8 +966,20 @@ Host: https://team-production-system.onrender.com ```JSON { + "pk": 1, "about_me": "Hi, I am so and so and do such and such", - "skills": "Python" + "skills": [ + "Python" + ], + "availabilities": [ + { + "pk": 1, + "mentor": 2, + "start_time": "2023-04-12T14:30:00Z", + "end_time": "2023-04-12T15:30:00Z" + } + ], + "team_number": 10 } ``` @@ -817,6 +1081,8 @@ Host: https://team-production-system.onrender.com --- +## Mentee Endpoints + ## View Mentee List (User Authentication **Required**) - View a list of all user with the mentee flag set to true @@ -1027,7 +1293,9 @@ No body returned to response --- -## Mentors Availabilty (User Authentication **Required**) +## Availability Endpoints + +## V1 | Get Mentors Availabilty (User Authentication **Required**) - Get mentor availabilty - This endpoint filters out any expired availabilty. Only shows availabilty that is in the future. @@ -1090,9 +1358,79 @@ Host: https://team-production-system.onrender.com --- -## Add Mentor Availabilty (User Authentication **Required**) +## V2 | View Mentor Availabilty List (User Authentication **Required**, Version Header **Required**) + +- Get mentor availabilty +- This endpoint filters out any expired availabilty +- Only shows availabilty with end_time in future. +- Availability reponse ordered from present to future +- Must pass version number in headers. + +```http +GET - https://team-production-system.onrender.com/availabilty/ +``` + +| Body | Type | Description | +| :----------- | :---------- | :----------------------------------------------- | +| `pk` | `int` | The pk of the availabilty | +| `mentor` | `int` | The pk of the mentor attached to the availabilty | +| `start_time` | `date-time` | Start time of the availabilty | +| `end_time` | `date-time` | Start time of the availabilty | +| `status` | `string` | Status of the availability | + +#### Request Sample: + +```JSON +GET /availabilty/ +Content-Type: json +Accept: version=v2 +Authorization: Required +Host: https://team-production-system.onrender.com + +{ + "" +} + +``` + +#### Response Example (200 OK) + +```JSON +[ + { + "pk": 19, + "mentor": 4, + "start_time": "1999-12-31T14:30:00Z", + "end_time": "1999-12-31T15:30:00Z" + }, + { + "pk": 20, + "mentor": 5, + "start_time": "1999-12-31T15:30:00Z", + "end_time": "1999-12-31T16:30:00Z" + }, + { + "pk": 21, + "mentor": 4, + "start_time": "1999-12-31T16:30:00Z", + "end_time": "1999-12-31T18:30:00Z" + }, + { + "pk": 22, + "mentor": 7, + "start_time": "1999-12-31T18:30:00Z", + "end_time": "1999-12-31T19:30:00Z" + } +] +``` + +--- + +## V1 | Add Mentor Availabilty (User Authentication **Required**) -- Add mentor availabilty (This endpoint filters out any expired availabilty. Only shows availabilty that is in the future.) +- Add mentor availabilty +- Start time must be in the future +- End time must be after start time ```http POST - https://team-production-system.onrender.com/availabilty/ @@ -1133,6 +1471,65 @@ Host: https://team-production-system.onrender.com --- +## V2 | Add Mentor Availabilty (User Authentication **Required**, Version Header **Required**) + +- Add mentor availabilty +- Availability saves to database in 30 min chunks +- Status defaults to 'Open' +- Must pass version number in headers. + +```http +POST - https://team-production-system.onrender.com/v2/availabilty/ +``` + +| Body | Type | Description | +| :----------- | :---------- | :----------------------------------------------- | +| `pk` | `int` | The pk of the availabilty | +| `mentor` | `int` | The pk of the mentor attached to the availabilty | +| `start_time` | `date-time` | Start time of the availabilty | +| `end_time` | `date-time` | Start time of the availabilty | +| `status` | `string` | Status of the availability | + +#### Request Sample: + +``` +POST /v2/availabilty/ +Content-Type: json +Accept: version=v2 +Authorization: Required +Host: https://team-production-system.onrender.com + +{ + "start_time": "1999-12-31T14:30:00Z", + "end_time": "1999-12-31T15:30:00Z" +} + +``` + +#### Response Example (201 Created) + +``` +[ + { + "pk": 23, + "mentor": 1, + "start_time": "1999-12-31T14:30:00Z", + "end_time": "1999-12-31T15:00:00Z", + "status": "Open" + }, + { + "pk": 24, + "mentor": 1, + "start_time": "1999-12-31T15:00:00Z", + "end_time": "1999-12-31T15:30:00Z", + "status": "Open" + }, + +] +``` + +--- + ## Delete Mentor Availabilty (User Authentication **Required**) - Delete a mentor availabilty @@ -1167,6 +1564,8 @@ No body returned to response --- +## Session Endpoints + ## Sessions (User Authentication **Required**) - Get a list of all sessions @@ -1425,6 +1824,8 @@ Host: https://team-production-system.onrender.com --- +## Notification Endpoints + ## Update Notification Settings (User Authentication **Required**) - Update user's notification settings diff --git a/config/__init__.py b/config/__init__.py index ab2f983..bb40e99 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -1,4 +1,5 @@ from __future__ import absolute_import + from .celery_settings import app as celery_app __all__ = ('celery_app',) diff --git a/config/celery_settings.py b/config/celery_settings.py index d727e0a..1111ea4 100644 --- a/config/celery_settings.py +++ b/config/celery_settings.py @@ -1,22 +1,12 @@ from __future__ import absolute_import + import os -# import ssl + from celery import Celery from django.conf import settings os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') -# for production -# app = Celery( -# "config", -# broker_use_ssl={ -# 'ssl_cert_reqs': ssl.CERT_NONE -# }, -# redis_backend_use_ssl={ -# 'ssl_cert_reqs': ssl.CERT_NONE -# } -# ) -# for development app = Celery('config') app.conf.beat_schedule = { diff --git a/config/settings.py b/config/settings.py index 5d816ea..76df789 100644 --- a/config/settings.py +++ b/config/settings.py @@ -10,18 +10,18 @@ https://docs.djangoproject.com/en/4.1/ref/settings/ """ -import environ from pathlib import Path -from corsheaders.defaults import default_headers + +import environ import sentry_sdk +from corsheaders.defaults import default_headers from sentry_sdk.integrations.django import DjangoIntegration - env = environ.Env( # set casting, default value DEBUG=(bool, False), USE_S3=(bool, False), - RENDER=(bool, False) + RENDER=(bool, False), ) @@ -43,6 +43,8 @@ ALLOWED_HOSTS = [] +if DEBUG: + ALLOWED_HOSTS.extend(['127.0.0.1', 'localhost']) INSTALLED_APPS = [ 'django.contrib.admin', @@ -67,8 +69,8 @@ MIDDLEWARE = [ "corsheaders.middleware.CorsMiddleware", 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', @@ -100,16 +102,7 @@ # Database # https://docs.djangoproject.com/en/4.1/ref/settings/#databases -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': env("DATABASE_NAME"), - 'USER': env("DATABASE_USER"), - 'PASSWORD': env("DATABASE_PASSWORD"), - 'HOST': env("DATABASE_HOST"), - 'PORT': env("DATABASE_PORT"), - }, -} +DATABASES = {'default': env.db()} # Password validation @@ -155,9 +148,6 @@ MEDIA_URL = "/media/" MEDIA_ROOT = BASE_DIR / "media" -if not DEBUG: - STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" - # Default primary key field type # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field @@ -166,8 +156,7 @@ if env("RENDER"): ALLOWED_HOSTS.append(env("RENDER_EXTERNAL_HOSTNAME")) DJANGO_SUPERUSER_USERNAME = env("DJANGO_SUPERUSER_USERNAME") - DJANGO_SUPERUSER_PASSWORD = env( - "DJANGO_SUPERUSER_PASSWORD") + DJANGO_SUPERUSER_PASSWORD = env("DJANGO_SUPERUSER_PASSWORD") DJANGO_SUPERUSER_EMAIL = env("DJANGO_SUPERUSER_EMAIL") AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID") AWS_SECRET_ACCESS_KEY = env("AWS_SECRET_ACCESS_KEY") @@ -176,9 +165,15 @@ AWS_S3_OBJECT_PARAMETERS = { 'CacheControl': 'max-age=86400', } - - -DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' + AWS_DEFAULT_ACL = 'public-read' + AWS_QUERYSTRING_AUTH = False + AWS_S3_FILE_OVERWRITE = False + STORAGES = { + 'default': {'BACKEND': 'storages.backends.s3.S3Storage'}, + 'staticfiles': { + 'BACKEND': 'storages.backends.s3boto3.S3StaticStorage', + }, + } AUTH_USER_MODEL = 'team_production_system.CustomUser' @@ -191,9 +186,6 @@ "http://localhost:3000", "http://localhost:3001", "https://momentors.dev", - - - ] CORS_ALLOW_HEADERS = list(default_headers) + [ @@ -204,6 +196,9 @@ 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.TokenAuthentication', ), + 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning', + 'DEFAULT_VERSION': 'v1', + 'ALLOWED_VERSIONS': ['v1', 'v2'], } DOMAIN = 'momentors.dev' @@ -216,13 +211,6 @@ 'PASSWORD_RESET_SHOW_EMAIL_NOT_FOUND': True, } -AWS_ACCESS_KEY_ID = env('AWS_ACCESS_KEY_ID') -AWS_SECRET_ACCESS_KEY = env('AWS_SECRET_ACCESS_KEY') -AWS_STORAGE_BUCKET_NAME = env('AWS_STORAGE_BUCKET_NAME') -AWS_QUERYSTRING_AUTH = False -AWS_S3_FILE_OVERWRITE = False - - EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_HOST = env('EMAIL_HOST') EMAIL_HOST_USER = env('EMAIL_HOST_USER') @@ -235,7 +223,6 @@ integrations=[ DjangoIntegration(), ], - # Set traces_sample_rate to 1.0 to capture 100% # of transactions for performance monitoring. # We recommend adjusting this value in production. diff --git a/config/urls.py b/config/urls.py index e1bb81c..fc0032d 100644 --- a/config/urls.py +++ b/config/urls.py @@ -14,8 +14,7 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path, include - +from django.urls import include, path urlpatterns = [ path('admin/', admin.site.urls), diff --git a/docker-compose.yaml b/docker-compose.yaml index a2f7833..5784d89 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -16,11 +16,7 @@ services: env_file: - ./config/.env environment: - - DATABASE_NAME=mentors - - DATABASE_USER=mentors - - DATABASE_PASSWORD=mentors - - DATABASE_HOST=db - - DATABASE_PORT=5432 + - DATABASE_URL=postgres://mentors:mentors@db:5432/mentors depends_on: - redis - db @@ -49,11 +45,7 @@ services: environment: - CELERY_BROKER_URL=redis://redis:6379 - CELERY_RESULT_BACKEND=redis://redis:6379 - - DATABASE_NAME=mentors - - DATABASE_USER=mentors - - DATABASE_PASSWORD=mentors - - DATABASE_HOST=db - - DATABASE_PORT=5432 + - DATABASE_URL=postgres://mentors:mentors@db:5432/mentors volumes: - .:/app depends_on: @@ -70,11 +62,7 @@ services: environment: - CELERY_BROKER_URL=redis://redis:6379 - CELERY_RESULT_BACKEND=redis://redis:6379 - - DATABASE_NAME=mentors - - DATABASE_USER=mentors - - DATABASE_PASSWORD=mentors - - DATABASE_HOST=db - - DATABASE_PORT=5432 + - DATABASE_URL=postgres://mentors:mentors@db:5432/mentors volumes: - .:/app depends_on: diff --git a/example.env b/example.env index 1088bba..c41e3bd 100644 --- a/example.env +++ b/example.env @@ -1,19 +1,25 @@ DATABASE_URL=Local Database + SECRET_KEY=Your Secret Key DEBUG=True or False + DJANGO_SUPERUSER_USERNAME=Example DJANGO_SUPERUSER_PASSWORD=Example DJANGO_SUPERUSER_EMAIL=Example + +CELERY_BROKER_URL=Example +CELERY_RESULT_BACKEND=Example + AWS_ACCESS_KEY_ID=Your AWS Key ID AWS_SECRET_ACCESS_KEY=Your AWS Secret key AWS_STORAGE_BUCKET_NAME=Your AWS storage bucket + EMAIL_HOST=Your email host EMAIL_HOST_USER=Example EMAIL_HOST_PASSWORD=Example + SENTRY_DSN=Example -CELERY_BROKER_URL=Example -CELERY_RESULT_BACKEND=Example (If you do not plan to create an Email server, AWS S3 bucket, and/or a sentry account, you will need to comment out lines 202 - 226 -while testing and uncomment them when creating a PR.) \ No newline at end of file +of config/settings.py while testing and uncomment them when creating a PR.) \ No newline at end of file diff --git a/manage.py b/manage.py index 8e7ac79..2125a97 100755 --- a/manage.py +++ b/manage.py @@ -11,10 +11,11 @@ def main(): from django.core.management import execute_from_command_line except ImportError as exc: raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" + 'Could not import Django. Are you sure it is installed and ' + 'available on your PYTHONPATH environment variable? Did you ' + 'forget to activate a virtual environment?' ) from exc + execute_from_command_line(sys.argv) diff --git a/media/random_photo/1.jpg b/media/random_photo/1.jpg new file mode 100644 index 0000000..caf611e Binary files /dev/null and b/media/random_photo/1.jpg differ diff --git a/media/random_photo/2.jpg b/media/random_photo/2.jpg new file mode 100644 index 0000000..d21d8ab Binary files /dev/null and b/media/random_photo/2.jpg differ diff --git a/media/random_photo/3.jpg b/media/random_photo/3.jpg new file mode 100644 index 0000000..b4ede8a Binary files /dev/null and b/media/random_photo/3.jpg differ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6b7007b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,9 @@ +[tool.black] +line-length = 88 +skip-string-normalization = 1 +[tool.isort] +profile = "black" +multi_line_output = 3 +include_trailing_comma = true +line_length = 88 +skip = ["./team_production_system/migrations/", ".gitignore", ".dockerignore"] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 54dda35..0000000 --- a/requirements.txt +++ /dev/null @@ -1,87 +0,0 @@ --i https://pypi.org/simple -amqp==5.1.1; python_version >= '3.6' -asgiref==3.7.2; python_version >= '3.7' -async-timeout==4.0.3; python_full_version <= '3.11.2' -attrs==23.1.0; python_version >= '3.7' -billiard==4.1.0; python_version >= '3.7' -boto3==1.28.57; python_version >= '3.7' -botocore==1.31.57; python_version >= '3.7' -celery==5.3.4; python_version >= '3.8' -certifi==2023.7.22; python_version >= '3.6' -cffi==1.16.0; python_version >= '3.8' -cfgv==3.4.0; python_version >= '3.8' -charset-normalizer==3.3.0; python_full_version >= '3.7.0' -click==8.1.7; python_version >= '3.7' -click-didyoumean==0.3.0; python_full_version >= '3.6.2' and python_full_version < '4.0.0' -click-plugins==1.1.1 -click-repl==0.3.0; python_version >= '3.6' -coverage==7.3.1; python_version >= '3.8' -cron-descriptor==1.4.0 -cryptography==41.0.4; python_version >= '3.7' -defusedxml==0.8.0rc2; python_version >= '3.6' -distlib==0.3.7 -django==4.2.5; python_version >= '3.8' -django-appconf==1.0.5; python_version >= '3.6' -django-celery-beat==2.5.0 -django-cors-headers==4.2.0; python_version >= '3.8' -django-environ==0.11.2; python_version >= '3.6' and python_version < '4' -django-extensions==3.2.3; python_version >= '3.6' -django-imagekit==5.0.0 -django-multiselectfield==0.1.12 -django-phonenumber-field==7.1.0; python_version >= '3.7' -django-storages==1.14.1; python_version >= '3.7' -django-templated-mail==1.1.1 -django-timezone-field==6.0.1; python_version >= '3.8' and python_version < '4.0' -djangorestframework==3.14.0; python_version >= '3.6' -djangorestframework-simplejwt==5.3.0; python_version >= '3.7' -djoser==2.2.0; python_version >= '3.8' and python_version < '4.0' -filelock==3.12.4; python_version >= '3.8' -flake8==6.1.0; python_full_version >= '3.8.1' -gunicorn==21.2.0; python_version >= '3.5' -identify==2.5.30; python_version >= '3.8' -idna==3.4; python_version >= '3.5' -inflection==0.5.1; python_version >= '3.5' -jmespath==1.0.1; python_version >= '3.7' -jsonschema==4.19.1; python_version >= '3.8' -jsonschema-specifications==2023.7.1; python_version >= '3.8' -kombu==5.3.2; python_version >= '3.8' -mccabe==0.7.0; python_version >= '3.6' -nodeenv==1.8.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6' -oauthlib==3.2.2; python_version >= '3.6' -packaging==23.2; python_version >= '3.7' -phonenumbers==8.13.22 -pilkit==3.0 -pillow==10.0.1; python_version >= '3.8' -platformdirs==3.10.0; python_version >= '3.7' -pre-commit==3.4.0; python_version >= '3.8' -prompt-toolkit==3.0.39; python_full_version >= '3.7.0' -psycopg2-binary==2.9.8; python_version >= '3.6' -pycodestyle==2.11.0; python_version >= '3.8' -pycparser==2.21 -pyflakes==3.1.0; python_version >= '3.8' -pyjwt==2.8.0; python_version >= '3.7' -python-crontab==3.0.0 -python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -python3-openid==3.2.0 -pytz==2023.3.post1 -pyyaml==6.0.1; python_version >= '3.6' -redis==5.0.1; python_version >= '3.7' -referencing==0.30.2; python_version >= '3.8' -requests==2.31.0; python_version >= '3.7' -requests-oauthlib==1.3.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -rpds-py==0.10.3; python_version >= '3.8' -s3transfer==0.7.0; python_version >= '3.7' -sentry-sdk[django]==1.31.0 -setuptools==68.2.2; python_version >= '3.8' -six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -social-auth-app-django==5.3.0; python_version >= '3.7' -social-auth-core==4.4.2; python_version >= '3.6' -sqlparse==0.4.4; python_version >= '3.5' -typing-extensions==4.8.0; python_version < '3.11' -tzdata==2023.3; python_version >= '2' -uritemplate==4.1.1; python_version >= '3.6' -urllib3==1.26.17; python_version >= '3.6' -vine==5.0.0; python_version >= '3.6' -virtualenv==20.24.5; python_version >= '3.7' -wcwidth==0.2.8 -whitenoise==6.5.0; python_version >= '3.7' diff --git a/scripts/commit-msg-lint.sh b/scripts/commit-msg-lint.sh index 4160001..da218b8 100755 --- a/scripts/commit-msg-lint.sh +++ b/scripts/commit-msg-lint.sh @@ -3,11 +3,12 @@ commit_msg_file=$1 commit_msg=$(cat $commit_msg_file) -regex="^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)\([a-z]+\): .+$" +regex_without_scope="^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert): .+$" +regex_with_scope="^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)\([a-z]+\): .+$" -if [[ ! $commit_msg =~ $regex ]]; then +if [[ ! $commit_msg =~ $regex_with_scope ]] && [[ ! $commit_msg =~ $regex_without_scope ]]; then echo "Error: Commit message does not match the required format!" - echo "Expected Format: " + echo "Expected Format: " echo "Example: chore(test): setup test" exit 1 -fi \ No newline at end of file +fi diff --git a/team_production_system/admin.py b/team_production_system/admin.py index d03f8ea..0175694 100644 --- a/team_production_system/admin.py +++ b/team_production_system/admin.py @@ -1,15 +1,25 @@ from django.contrib import admin +from django.contrib.auth.admin import UserAdmin + from .models import ( - Mentor, - Mentee, + Availability, CustomUser, + Mentee, + Mentor, + NotificationSettings, Session, - Availability, - NotificationSettings ) -# admin.site.register(UserAdmin) -admin.site.register(CustomUser) + +class AvailabilityAdmin(admin.ModelAdmin): + readonly_fields = ('created_at', 'modified_at') + + +class SessionAdmin(admin.ModelAdmin): + readonly_fields = ('created_at', 'modified_at') + + +admin.site.register(CustomUser, UserAdmin) admin.site.register(Mentor) admin.site.register(Mentee) admin.site.register(Session) diff --git a/team_production_system/helpers.py b/team_production_system/helpers.py new file mode 100644 index 0000000..b43226a --- /dev/null +++ b/team_production_system/helpers.py @@ -0,0 +1,62 @@ +# helper.py +# Purpose: Helper functions for the team_production_system app. + +from datetime import timedelta + +from django.utils import timezone +from django.utils.dateparse import parse_datetime + +from team_production_system.models import Availability + + +# Create a list of Availability objects in 30 minutes chunks for a time range +def create_30_min_availabilities(start_time_str, end_time_str, mentor): + chunk_size = timedelta(minutes=30) + start_time = parse_datetime(start_time_str) + end_time = parse_datetime(end_time_str) + + availabilities = [] + while start_time < end_time: + end_time_new = start_time + chunk_size + if end_time_new > end_time: + break + + availability = { + 'start_time': start_time, + 'end_time': end_time_new, + 'mentor': mentor.user.pk, + 'status': 'Open', + } + availabilities.append(availability) + start_time += chunk_size + + return availabilities + + +# Check if the mentor has any overlapping availabilities +def is_overlapping_availabilities(mentor, data): + """ + Check if there is any overlap with existing availabilities. + """ + start_time = data['start_time'] + end_time = data['end_time'] + overlapping_availabilities = Availability.objects.filter( + mentor=mentor, start_time__lt=end_time, end_time__gt=start_time + ) + return overlapping_availabilities.exists() + + +# Check if the start_time is in the future +def is_valid_start_time(start_time): + """ + Check that the start_time is in the future. + """ + return start_time > timezone.now() + + +# Check if the end_time is after the start_time +def is_valid_end_time(start_time, end_time): + """ + Check that the end_time is after the start_time. + """ + return end_time > start_time diff --git a/team_production_system/management/commands/add_superuser.py b/team_production_system/management/commands/add_superuser.py index 2bf1753..4ce6174 100644 --- a/team_production_system/management/commands/add_superuser.py +++ b/team_production_system/management/commands/add_superuser.py @@ -1,9 +1,9 @@ - import os + from django.core.management.base import BaseCommand -from team_production_system.models import CustomUser -from config import settings +from config import settings +from team_production_system.models import CustomUser # To run this management command: @@ -13,16 +13,20 @@ class Command(BaseCommand): def handle(self, *args, **options): if os.environ.get('RENDER'): - user, created = CustomUser.objects.get_or_create( + user, created = CustomUser.objects.get_or_create( username=settings.DJANGO_SUPERUSER_USERNAME ) - if created: - user.email = settings.DJANGO_SUPERUSER_EMAIL - user.set_password(settings.DJANGO_SUPERUSER_PASSWORD) - user.is_superuser = True - user.is_staff = True - user.save() - msg = self.style.SUCCESS(f"Superuser {settings.DJANGO_SUPERUSER_USERNAME} added to database.") - else: - msg = self.style.WARNING(f"Superuser {settings.DJANGO_SUPERUSER_USERNAME} already exists.") - self.stdout.write(msg) \ No newline at end of file + if created: + user.email = settings.DJANGO_SUPERUSER_EMAIL + user.set_password(settings.DJANGO_SUPERUSER_PASSWORD) + user.is_superuser = True + user.is_staff = True + user.save() + msg = self.style.SUCCESS( + f"Superuser {settings.DJANGO_SUPERUSER_USERNAME} added to database." + ) + else: + msg = self.style.WARNING( + f"Superuser {settings.DJANGO_SUPERUSER_USERNAME} already exists." + ) + self.stdout.write(msg) diff --git a/team_production_system/migrations/0001_initial.py b/team_production_system/migrations/0001_initial.py index e8813f7..f50b1ef 100644 --- a/team_production_system/migrations/0001_initial.py +++ b/team_production_system/migrations/0001_initial.py @@ -1,17 +1,16 @@ # Generated by Django 4.1.7 on 2023-04-10 14:16 -from django.conf import settings import django.contrib.auth.models import django.contrib.auth.validators -from django.db import migrations, models import django.db.models.deletion import django.utils.timezone import multiselectfield.db.fields import phonenumber_field.modelfields +from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [ @@ -22,23 +21,97 @@ class Migration(migrations.Migration): migrations.CreateModel( name='CustomUser', fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + 'id', + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ( + 'last_login', + models.DateTimeField( + blank=True, null=True, verbose_name='last login' + ), + ), + ( + 'is_superuser', + models.BooleanField( + default=False, + help_text='Designates that this user has all permissions without explicitly assigning them.', + verbose_name='superuser status', + ), + ), + ( + 'username', + models.CharField( + error_messages={ + 'unique': 'A user with that username already exists.' + }, + help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name='username', + ), + ), + ( + 'is_staff', + models.BooleanField( + default=False, + help_text='Designates whether the user can log into this admin site.', + verbose_name='staff status', + ), + ), + ( + 'date_joined', + models.DateTimeField( + default=django.utils.timezone.now, verbose_name='date joined' + ), + ), ('is_mentor', models.BooleanField(default=False)), ('is_mentee', models.BooleanField(default=False)), ('is_active', models.BooleanField(default=True)), ('first_name', models.CharField(max_length=75)), ('last_name', models.CharField(max_length=75)), ('email', models.EmailField(max_length=75, unique=True)), - ('phone_number', phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None, unique=True)), - ('profile_photo', models.ImageField(blank=True, null=True, upload_to='profile_photo')), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ( + 'phone_number', + phonenumber_field.modelfields.PhoneNumberField( + blank=True, max_length=128, null=True, region=None, unique=True + ), + ), + ( + 'profile_photo', + models.ImageField(blank=True, null=True, upload_to='profile_photo'), + ), + ( + 'groups', + models.ManyToManyField( + blank=True, + help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', + related_name='user_set', + related_query_name='user', + to='auth.group', + verbose_name='groups', + ), + ), + ( + 'user_permissions', + models.ManyToManyField( + blank=True, + help_text='Specific permissions for this user.', + related_name='user_set', + related_query_name='user', + to='auth.permission', + verbose_name='user permissions', + ), + ), ], options={ 'verbose_name': 'user', @@ -52,7 +125,15 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Availability', fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + 'id', + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), ('start_time', models.DateTimeField()), ('end_time', models.DateTimeField()), ], @@ -60,47 +141,142 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Mentee', fields=[ - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), + ( + 'user', + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + primary_key=True, + serialize=False, + to=settings.AUTH_USER_MODEL, + ), + ), ('team_number', models.IntegerField()), ], ), migrations.CreateModel( name='Mentor', fields=[ - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), + ( + 'user', + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + primary_key=True, + serialize=False, + to=settings.AUTH_USER_MODEL, + ), + ), ('about_me', models.TextField(max_length=1000)), - ('skills', multiselectfield.db.fields.MultiSelectField(choices=[('HTML', 'HTML'), ('CSS', 'CSS'), ('JavaScript', 'JavaScript'), ('React', 'React'), ('Python', 'Python'), ('Django', 'Django'), ('Django REST', 'Django REST')], max_length=52)), + ( + 'skills', + multiselectfield.db.fields.MultiSelectField( + choices=[ + ('HTML', 'HTML'), + ('CSS', 'CSS'), + ('JavaScript', 'JavaScript'), + ('React', 'React'), + ('Python', 'Python'), + ('Django', 'Django'), + ('Django REST', 'Django REST'), + ], + max_length=52, + ), + ), ], ), migrations.CreateModel( name='Notification', fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + 'id', + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), ('message', models.CharField(max_length=500)), ('created_at', models.DateTimeField(auto_now_add=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)), + ( + 'user', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='notifications', + to=settings.AUTH_USER_MODEL, + ), + ), ], ), migrations.CreateModel( name='Session', fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + 'id', + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), ('start_time', models.DateTimeField()), ('project', models.CharField(max_length=500)), ('help_text', models.TextField(max_length=500)), ('git_link', models.URLField()), ('created_at', models.DateTimeField(auto_now_add=True)), ('confirmed', models.BooleanField(default=False)), - ('status', models.CharField(choices=[('Pending', 'Pending'), ('Confirmed', 'Confirmed'), ('Canceled', 'Canceled'), ('Completed', 'Completed')], default='Pending', max_length=10)), - ('session_length', models.IntegerField(choices=[(30, '30 minutes'), (60, '60 minutes')], default=30)), - ('mentor_availability', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mentor_session', to='team_production_system.availability')), - ('mentee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mentee_session', to='team_production_system.mentee')), - ('mentor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mentor_session', to='team_production_system.mentor')), + ( + 'status', + models.CharField( + choices=[ + ('Pending', 'Pending'), + ('Confirmed', 'Confirmed'), + ('Canceled', 'Canceled'), + ('Completed', 'Completed'), + ], + default='Pending', + max_length=10, + ), + ), + ( + 'session_length', + models.IntegerField( + choices=[(30, '30 minutes'), (60, '60 minutes')], default=30 + ), + ), + ( + 'mentor_availability', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='mentor_session', + to='team_production_system.availability', + ), + ), + ( + 'mentee', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='mentee_session', + to='team_production_system.mentee', + ), + ), + ( + 'mentor', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='mentor_session', + to='team_production_system.mentor', + ), + ), ], ), migrations.AddField( model_name='availability', name='mentor', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mentor_availability', to='team_production_system.mentor'), + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='mentor_availability', + to='team_production_system.mentor', + ), ), ] diff --git a/team_production_system/migrations/0002_alter_mentee_team_number_alter_mentor_about_me_and_more.py b/team_production_system/migrations/0002_alter_mentee_team_number_alter_mentor_about_me_and_more.py index fd3f253..1df8181 100644 --- a/team_production_system/migrations/0002_alter_mentee_team_number_alter_mentor_about_me_and_more.py +++ b/team_production_system/migrations/0002_alter_mentee_team_number_alter_mentor_about_me_and_more.py @@ -1,11 +1,10 @@ # Generated by Django 4.1.7 on 2023-05-01 14:42 -from django.db import migrations, models import multiselectfield.db.fields +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ('team_production_system', '0001_initial'), ] @@ -24,6 +23,18 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='mentor', name='skills', - field=multiselectfield.db.fields.MultiSelectField(choices=[('HTML', 'HTML'), ('CSS', 'CSS'), ('JavaScript', 'JavaScript'), ('React', 'React'), ('Python', 'Python'), ('Django', 'Django'), ('Django REST', 'Django REST')], default='HTML', max_length=52), + field=multiselectfield.db.fields.MultiSelectField( + choices=[ + ('HTML', 'HTML'), + ('CSS', 'CSS'), + ('JavaScript', 'JavaScript'), + ('React', 'React'), + ('Python', 'Python'), + ('Django', 'Django'), + ('Django REST', 'Django REST'), + ], + default='HTML', + max_length=52, + ), ), ] diff --git a/team_production_system/migrations/0003_alter_customuser_phone_number.py b/team_production_system/migrations/0003_alter_customuser_phone_number.py index e55ccfb..321a524 100644 --- a/team_production_system/migrations/0003_alter_customuser_phone_number.py +++ b/team_production_system/migrations/0003_alter_customuser_phone_number.py @@ -1,19 +1,28 @@ # Generated by Django 4.1.7 on 2023-05-04 19:47 -from django.db import migrations import phonenumber_field.modelfields +from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('team_production_system', '0002_alter_mentee_team_number_alter_mentor_about_me_and_more'), + ( + 'team_production_system', + '0002_alter_mentee_team_number_alter_mentor_about_me_and_more', + ), ] operations = [ migrations.AlterField( model_name='customuser', name='phone_number', - field=phonenumber_field.modelfields.PhoneNumberField(blank=True, default='', max_length=128, null=True, region=None, unique=True), + field=phonenumber_field.modelfields.PhoneNumberField( + blank=True, + default='', + max_length=128, + null=True, + region=None, + unique=True, + ), ), ] diff --git a/team_production_system/migrations/0004_alter_customuser_phone_number.py b/team_production_system/migrations/0004_alter_customuser_phone_number.py index 0157977..ae6816f 100644 --- a/team_production_system/migrations/0004_alter_customuser_phone_number.py +++ b/team_production_system/migrations/0004_alter_customuser_phone_number.py @@ -1,11 +1,10 @@ # Generated by Django 4.1.7 on 2023-05-04 21:00 -from django.db import migrations import phonenumber_field.modelfields +from django.db import migrations class Migration(migrations.Migration): - dependencies = [ ('team_production_system', '0003_alter_customuser_phone_number'), ] @@ -14,6 +13,13 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='customuser', name='phone_number', - field=phonenumber_field.modelfields.PhoneNumberField(blank=True, default=' ', max_length=128, null=True, region=None, unique=True), + field=phonenumber_field.modelfields.PhoneNumberField( + blank=True, + default=' ', + max_length=128, + null=True, + region=None, + unique=True, + ), ), ] diff --git a/team_production_system/migrations/0005_alter_customuser_phone_number.py b/team_production_system/migrations/0005_alter_customuser_phone_number.py index f3db5cc..a0e9ecf 100644 --- a/team_production_system/migrations/0005_alter_customuser_phone_number.py +++ b/team_production_system/migrations/0005_alter_customuser_phone_number.py @@ -1,11 +1,10 @@ # Generated by Django 4.1.7 on 2023-05-05 01:05 -from django.db import migrations import phonenumber_field.modelfields +from django.db import migrations class Migration(migrations.Migration): - dependencies = [ ('team_production_system', '0004_alter_customuser_phone_number'), ] @@ -14,6 +13,13 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='customuser', name='phone_number', - field=phonenumber_field.modelfields.PhoneNumberField(blank=True, default=None, max_length=128, null=True, region=None, unique=True), + field=phonenumber_field.modelfields.PhoneNumberField( + blank=True, + default=None, + max_length=128, + null=True, + region=None, + unique=True, + ), ), ] diff --git a/team_production_system/migrations/0006_alter_mentor_skills.py b/team_production_system/migrations/0006_alter_mentor_skills.py index 0bdb608..65a6232 100644 --- a/team_production_system/migrations/0006_alter_mentor_skills.py +++ b/team_production_system/migrations/0006_alter_mentor_skills.py @@ -1,11 +1,10 @@ # Generated by Django 4.2.1 on 2023-05-24 21:46 -from django.db import migrations import multiselectfield.db.fields +from django.db import migrations class Migration(migrations.Migration): - dependencies = [ ('team_production_system', '0005_alter_customuser_phone_number'), ] @@ -14,6 +13,28 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='mentor', name='skills', - field=multiselectfield.db.fields.MultiSelectField(choices=[('AWS S3', 'AWS S3'), ('Bootstrap', 'Bootstrap'), ('CSS', 'CSS'), ('Django', 'Django'), ('Git', 'Git'), ('GitHub', 'GitHub'), ('HTML', 'HTML'), ('Insomnia', 'Insomnia'), ('JavaScript', 'JavaScript'), ('MUI', 'MUI'), ('Other', 'Other'), ('PostgreSQL', 'PostgreSQL'), ('Postico', 'Postico'), ('Python', 'Python'), ('React', 'React'), ('SQL', 'SQL'), ('Time Management', 'Time Management')], default='HTML', max_length=52), + field=multiselectfield.db.fields.MultiSelectField( + choices=[ + ('AWS S3', 'AWS S3'), + ('Bootstrap', 'Bootstrap'), + ('CSS', 'CSS'), + ('Django', 'Django'), + ('Git', 'Git'), + ('GitHub', 'GitHub'), + ('HTML', 'HTML'), + ('Insomnia', 'Insomnia'), + ('JavaScript', 'JavaScript'), + ('MUI', 'MUI'), + ('Other', 'Other'), + ('PostgreSQL', 'PostgreSQL'), + ('Postico', 'Postico'), + ('Python', 'Python'), + ('React', 'React'), + ('SQL', 'SQL'), + ('Time Management', 'Time Management'), + ], + default='HTML', + max_length=52, + ), ), ] diff --git a/team_production_system/migrations/0007_notificationsettings_delete_notification.py b/team_production_system/migrations/0007_notificationsettings_delete_notification.py index 6e2237c..a93f4a8 100644 --- a/team_production_system/migrations/0007_notificationsettings_delete_notification.py +++ b/team_production_system/migrations/0007_notificationsettings_delete_notification.py @@ -1,12 +1,11 @@ # Generated by Django 4.2.1 on 2023-06-01 17:13 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ ('team_production_system', '0006_alter_mentor_skills'), ] @@ -15,13 +14,28 @@ class Migration(migrations.Migration): migrations.CreateModel( name='NotificationSettings', fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + 'id', + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), ('session_requested', models.BooleanField(default=False)), ('session_confirmed', models.BooleanField(default=False)), ('session_canceled', models.BooleanField(default=False)), ('fifteen_minute_alert', models.BooleanField(default=False)), ('sixty_minute_alert', models.BooleanField(default=False)), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='notification_settings', to=settings.AUTH_USER_MODEL)), + ( + 'user', + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name='notification_settings', + to=settings.AUTH_USER_MODEL, + ), + ), ], ), migrations.DeleteModel( diff --git a/team_production_system/migrations/0008_alter_mentor_skills.py b/team_production_system/migrations/0008_alter_mentor_skills.py index 4d16683..bd4ad14 100644 --- a/team_production_system/migrations/0008_alter_mentor_skills.py +++ b/team_production_system/migrations/0008_alter_mentor_skills.py @@ -1,11 +1,10 @@ # Generated by Django 4.2.1 on 2023-06-05 19:16 -from django.db import migrations import multiselectfield.db.fields +from django.db import migrations class Migration(migrations.Migration): - dependencies = [ ('team_production_system', '0007_notificationsettings_delete_notification'), ] @@ -14,6 +13,28 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='mentor', name='skills', - field=multiselectfield.db.fields.MultiSelectField(choices=[('AWS S3', 'AWS S3'), ('Bootstrap', 'Bootstrap'), ('CSS', 'CSS'), ('Django', 'Django'), ('Git', 'Git'), ('GitHub', 'GitHub'), ('HTML', 'HTML'), ('Insomnia', 'Insomnia'), ('JavaScript', 'JavaScript'), ('MUI', 'MUI'), ('Other', 'Other'), ('PostgreSQL', 'PostgreSQL'), ('Postico', 'Postico'), ('Python', 'Python'), ('React', 'React'), ('SQL', 'SQL'), ('Time Management', 'Time Management')], default='HTML', max_length=157), + field=multiselectfield.db.fields.MultiSelectField( + choices=[ + ('AWS S3', 'AWS S3'), + ('Bootstrap', 'Bootstrap'), + ('CSS', 'CSS'), + ('Django', 'Django'), + ('Git', 'Git'), + ('GitHub', 'GitHub'), + ('HTML', 'HTML'), + ('Insomnia', 'Insomnia'), + ('JavaScript', 'JavaScript'), + ('MUI', 'MUI'), + ('Other', 'Other'), + ('PostgreSQL', 'PostgreSQL'), + ('Postico', 'Postico'), + ('Python', 'Python'), + ('React', 'React'), + ('SQL', 'SQL'), + ('Time Management', 'Time Management'), + ], + default='HTML', + max_length=157, + ), ), ] diff --git a/team_production_system/migrations/0009_availability_availability_constraint.py b/team_production_system/migrations/0009_availability_availability_constraint.py index 3d1de60..2810905 100644 --- a/team_production_system/migrations/0009_availability_availability_constraint.py +++ b/team_production_system/migrations/0009_availability_availability_constraint.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ('team_production_system', '0008_alter_mentor_skills'), ] @@ -12,6 +11,8 @@ class Migration(migrations.Migration): operations = [ migrations.AddConstraint( model_name='availability', - constraint=models.UniqueConstraint(fields=('mentor', 'start_time'), name='availability_constraint'), + constraint=models.UniqueConstraint( + fields=('mentor', 'start_time'), name='availability_constraint' + ), ), ] diff --git a/team_production_system/migrations/0010_alter_mentor_skills.py b/team_production_system/migrations/0010_alter_mentor_skills.py index bc144b1..5dcfdc6 100644 --- a/team_production_system/migrations/0010_alter_mentor_skills.py +++ b/team_production_system/migrations/0010_alter_mentor_skills.py @@ -1,11 +1,10 @@ # Generated by Django 4.2.4 on 2023-09-01 19:43 -from django.db import migrations import multiselectfield.db.fields +from django.db import migrations class Migration(migrations.Migration): - dependencies = [ ('team_production_system', '0009_availability_availability_constraint'), ] @@ -14,6 +13,34 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='mentor', name='skills', - field=multiselectfield.db.fields.MultiSelectField(choices=[('AI', 'AI'), ('AWS S3', 'AWS S3'), ('Bootstrap', 'Bootstrap'), ('Career Help', 'Career Help'), ('CSS', 'CSS'), ('Django', 'Django'), ('FastAPI', 'FastAPI'), ('Git', 'Git'), ('GitHub', 'GitHub'), ('HTML', 'HTML'), ('Insomnia', 'Insomnia'), ('Interview Help', 'Interview Help'), ('JavaScript', 'JavaScript'), ('MUI', 'MUI'), ('Other', 'Other'), ('PostgreSQL', 'PostgreSQL'), ('Postico', 'Postico'), ('Python', 'Python'), ('React', 'React'), ('Resume Help', 'Resume Help'), ('SQL', 'SQL'), ('Time Management', 'Time Management'), ('Vue.js', 'Vue.js')], default='HTML', max_length=157), + field=multiselectfield.db.fields.MultiSelectField( + choices=[ + ('AI', 'AI'), + ('AWS S3', 'AWS S3'), + ('Bootstrap', 'Bootstrap'), + ('Career Help', 'Career Help'), + ('CSS', 'CSS'), + ('Django', 'Django'), + ('FastAPI', 'FastAPI'), + ('Git', 'Git'), + ('GitHub', 'GitHub'), + ('HTML', 'HTML'), + ('Insomnia', 'Insomnia'), + ('Interview Help', 'Interview Help'), + ('JavaScript', 'JavaScript'), + ('MUI', 'MUI'), + ('Other', 'Other'), + ('PostgreSQL', 'PostgreSQL'), + ('Postico', 'Postico'), + ('Python', 'Python'), + ('React', 'React'), + ('Resume Help', 'Resume Help'), + ('SQL', 'SQL'), + ('Time Management', 'Time Management'), + ('Vue.js', 'Vue.js'), + ], + default='HTML', + max_length=157, + ), ), ] diff --git a/team_production_system/migrations/0011_mentor_team_number.py b/team_production_system/migrations/0011_mentor_team_number.py index 3834b2b..f146821 100644 --- a/team_production_system/migrations/0011_mentor_team_number.py +++ b/team_production_system/migrations/0011_mentor_team_number.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ('team_production_system', '0010_alter_mentor_skills'), ] diff --git a/team_production_system/migrations/0012_remove_availability_availability_constraint_and_more.py b/team_production_system/migrations/0012_remove_availability_availability_constraint_and_more.py new file mode 100644 index 0000000..3c383f8 --- /dev/null +++ b/team_production_system/migrations/0012_remove_availability_availability_constraint_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2.5 on 2023-10-10 20:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("team_production_system", "0011_mentor_team_number"), + ] + + operations = [ + migrations.RemoveConstraint( + model_name="availability", + name="availability_constraint", + ), + migrations.AddField( + model_name="availability", + name="status", + field=models.CharField( + choices=[ + ("Open", "Open"), + ("Requested", "Requested"), + ("Confirmed", "Confirmed"), + ], + default="Open", + max_length=10, + ), + ), + migrations.AddConstraint( + model_name="availability", + constraint=models.UniqueConstraint( + fields=("mentor", "start_time"), + name="availability_constraint", + violation_error_message="Availability already exists.", + ), + ), + ] diff --git a/team_production_system/migrations/0013_availability_created_at_availability_modified_at_and_more.py b/team_production_system/migrations/0013_availability_created_at_availability_modified_at_and_more.py new file mode 100644 index 0000000..99b7b22 --- /dev/null +++ b/team_production_system/migrations/0013_availability_created_at_availability_modified_at_and_more.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.6 on 2023-10-11 04:24 + +import datetime + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ( + "team_production_system", + "0012_remove_availability_availability_constraint_and_more", + ), + ] + + operations = [ + migrations.AddField( + model_name="availability", + name="created_at", + field=models.DateTimeField( + auto_now_add=True, + default=datetime.datetime( + 2023, 10, 11, 4, 24, 26, 645977, tzinfo=datetime.timezone.utc + ), + ), + preserve_default=False, + ), + migrations.AddField( + model_name="availability", + name="modified_at", + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name="session", + name="modified_at", + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/team_production_system/models/__init__.py b/team_production_system/models/__init__.py index ed72613..3be962c 100644 --- a/team_production_system/models/__init__.py +++ b/team_production_system/models/__init__.py @@ -2,8 +2,8 @@ from .availability import Availability +from .custom_user import CustomUser from .mentee import Mentee from .mentor import Mentor from .notification_settings import NotificationSettings from .session import Session -from .custom_user import CustomUser diff --git a/team_production_system/models/availability.py b/team_production_system/models/availability.py index f6a3a3d..f0cc69b 100644 --- a/team_production_system/models/availability.py +++ b/team_production_system/models/availability.py @@ -1,24 +1,34 @@ -from .mentor import Mentor -from django.db.models.constraints import UniqueConstraint from django.db import models +from django.db.models.constraints import UniqueConstraint + +from .mentor import Mentor -# Allow mentors to set their avaliabiltiy +# Allow mentors to set their availability class Availability(models.Model): + STATUS_CHOICES = [ + ('Open', 'Open'), + ('Requested', 'Requested'), + ('Confirmed', 'Confirmed'), + ] + mentor = models.ForeignKey( - Mentor, on_delete=models.CASCADE, related_name='mentor_availability') + Mentor, on_delete=models.CASCADE, related_name='mentor_availability' + ) start_time = models.DateTimeField() end_time = models.DateTimeField() + status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='Open') + created_at = models.DateTimeField(auto_now_add=True) + modified_at = models.DateTimeField(auto_now=True) class Meta: constraints = [ UniqueConstraint( fields=['mentor', 'start_time'], - name='availability_constraint') + name='availability_constraint', + violation_error_message='Availability already exists.', + ) ] def __str__(self): - return ( - f"{self.mentor} is available from " - f"{self.start_time} to {self.end_time}." - ) + return f'{self.mentor} is available from {self.start_time} to {self.end_time}.' diff --git a/team_production_system/models/custom_user.py b/team_production_system/models/custom_user.py index 07071eb..cf2237c 100644 --- a/team_production_system/models/custom_user.py +++ b/team_production_system/models/custom_user.py @@ -1,8 +1,10 @@ -from django.db import models +import random + from django.contrib.auth.models import AbstractUser -from phonenumber_field.modelfields import PhoneNumberField from django.core.files.storage import default_storage -import random +from django.db import models +from phonenumber_field.modelfields import PhoneNumberField + from .notification_settings import NotificationSettings @@ -14,10 +16,8 @@ class CustomUser(AbstractUser): first_name = models.CharField(max_length=75) last_name = models.CharField(max_length=75) email = models.EmailField(max_length=75, unique=True) - phone_number = PhoneNumberField( - null=True, blank=True, unique=True, default=None) - profile_photo = models.ImageField( - upload_to='profile_photo', blank=True, null=True) + phone_number = PhoneNumberField(null=True, blank=True, unique=True, default=None) + profile_photo = models.ImageField(upload_to='profile_photo', blank=True, null=True) def __str__(self): return self.username diff --git a/team_production_system/models/mentee.py b/team_production_system/models/mentee.py index 103cc1c..91e130d 100644 --- a/team_production_system/models/mentee.py +++ b/team_production_system/models/mentee.py @@ -1,11 +1,11 @@ from django.db import models + from .custom_user import CustomUser # Model for mentees to input their team class Mentee(models.Model): - user = models.OneToOneField( - CustomUser, on_delete=models.CASCADE, primary_key=True) + user = models.OneToOneField(CustomUser, on_delete=models.CASCADE, primary_key=True) team_number = models.IntegerField(default=0) def __str__(self): diff --git a/team_production_system/models/mentor.py b/team_production_system/models/mentor.py index 89a4cf9..951dcd3 100644 --- a/team_production_system/models/mentor.py +++ b/team_production_system/models/mentor.py @@ -1,12 +1,12 @@ from django.db import models -from .custom_user import CustomUser from multiselectfield import MultiSelectField +from .custom_user import CustomUser + # The mentor model that allows the mentor to select skills # they know and information about them class Mentor(models.Model): - SKILLS_CHOICES = [ ('AI', 'AI'), ('AWS S3', 'AWS S3'), @@ -33,12 +33,12 @@ class Mentor(models.Model): ('Vue.js', 'Vue.js'), ] - user = models.OneToOneField( - CustomUser, on_delete=models.CASCADE, primary_key=True) + user = models.OneToOneField(CustomUser, on_delete=models.CASCADE, primary_key=True) about_me = models.TextField(max_length=1000, default='') team_number = models.IntegerField(default=0) - skills = MultiSelectField(choices=SKILLS_CHOICES, - max_choices=19, max_length=157, default='HTML') + skills = MultiSelectField( + choices=SKILLS_CHOICES, max_choices=19, max_length=157, default='HTML' + ) def __str__(self): return self.user.username diff --git a/team_production_system/models/notification_settings.py b/team_production_system/models/notification_settings.py index 9a68379..26ffe07 100644 --- a/team_production_system/models/notification_settings.py +++ b/team_production_system/models/notification_settings.py @@ -5,9 +5,10 @@ # they have a session requested, confirmed, or canceled. class NotificationSettings(models.Model): user = models.OneToOneField( - "team_production_system.CustomUser", + 'team_production_system.CustomUser', on_delete=models.CASCADE, - related_name='notification_settings') + related_name='notification_settings', + ) session_requested = models.BooleanField(default=False) session_confirmed = models.BooleanField(default=False) session_canceled = models.BooleanField(default=False) diff --git a/team_production_system/models/session.py b/team_production_system/models/session.py index 6118243..71d5a00 100644 --- a/team_production_system/models/session.py +++ b/team_production_system/models/session.py @@ -1,52 +1,53 @@ -from django.db import models +import secrets +from datetime import timedelta + +import pytz from django.conf import settings from django.core.mail import send_mail +from django.db import models + +from .availability import Availability from .mentee import Mentee from .mentor import Mentor -from .availability import Availability -from datetime import timedelta -import secrets -import pytz # The session model allows the mentee to setup a session and # allows both mentee and mentor see their sessions they have scheduled class Session(models.Model): - mentor = models.ForeignKey(Mentor, on_delete=models.CASCADE, - related_name='mentor_session') + mentor = models.ForeignKey( + Mentor, on_delete=models.CASCADE, related_name='mentor_session' + ) mentor_availability = models.ForeignKey( - Availability, on_delete=models.CASCADE, related_name='mentor_session') + Availability, on_delete=models.CASCADE, related_name='mentor_session' + ) mentee = models.ForeignKey( - Mentee, on_delete=models.CASCADE, related_name='mentee_session') + Mentee, on_delete=models.CASCADE, related_name='mentee_session' + ) start_time = models.DateTimeField() project = models.CharField(max_length=500) help_text = models.TextField(max_length=500) git_link = models.URLField(max_length=200) - created_at = models.DateTimeField(auto_now_add=True) confirmed = models.BooleanField(default=False) status_choices = [ ('Pending', 'Pending'), ('Confirmed', 'Confirmed'), ('Canceled', 'Canceled'), - ('Completed', 'Completed') + ('Completed', 'Completed'), ] # The mentee will be able to schedule a 30 minute or 60 minute session. - status = models.CharField( - max_length=10, choices=status_choices, default='Pending') - session_length_choices = [ - (30, '30 minutes'), - (60, '60 minutes') - ] - session_length = models.IntegerField( - choices=session_length_choices, default=30) + status = models.CharField(max_length=10, choices=status_choices, default='Pending') + session_length_choices = [(30, '30 minutes'), (60, '60 minutes')] + session_length = models.IntegerField(choices=session_length_choices, default=30) + created_at = models.DateTimeField(auto_now_add=True) + modified_at = models.DateTimeField(auto_now=True) def end_time(self): return self.start_time + timedelta(minutes=self.session_length) def __str__(self): return ( - f"{self.mentor_availability.mentor.user.username} " - f"session with {self.mentee.user.username} is ({self.status})" + f'{self.mentor_availability.mentor.user.username} ' + f'session with {self.mentee.user.username} is ({self.status})' ) # Notify a mentor that a mentee has requested a session @@ -113,7 +114,7 @@ def mentee_confirm_notify(self, meeting_link): f'Here is the link to your session: {meeting_link}' ), from_email=settings.EMAIL_HOST_USER, - recipient_list=[self.mentee.user.email] + recipient_list=[self.mentee.user.email], ) # Notify the mentor when the requested session has been confirmed @@ -139,7 +140,7 @@ def mentor_confirm_notify(self, meeting_link): f'Here is the link to your session: {meeting_link}' ), from_email=settings.EMAIL_HOST_USER, - recipient_list=[self.mentor.user.email] + recipient_list=[self.mentor.user.email], ) # Notify a mentor that a mentee has canceled a scheduled session diff --git a/team_production_system/serializers/__init__.py b/team_production_system/serializers/__init__.py index 0b810bd..491ba90 100644 --- a/team_production_system/serializers/__init__.py +++ b/team_production_system/serializers/__init__.py @@ -1,10 +1,10 @@ # flake8: noqa -from .availability import AvailabilitySerializer +from .availability import AvailabilitySerializer, AvailabilitySerializerV2 from .custom_user import CustomUserSerializer -from .mentor_profile import MentorProfileSerializer -from .mentor_list import MentorListSerializer -from .mentee_profile import MenteeProfileSerializer from .mentee_list import MenteeListSerializer -from .session import SessionSerializer +from .mentee_profile import MenteeProfileSerializer +from .mentor_list import MentorListSerializer +from .mentor_profile import MentorProfileSerializer from .notification_settings import NotificationSettingsSerializer +from .session import SessionSerializer diff --git a/team_production_system/serializers/availability.py b/team_production_system/serializers/availability.py index edfb406..fbffc7c 100644 --- a/team_production_system/serializers/availability.py +++ b/team_production_system/serializers/availability.py @@ -1,62 +1,68 @@ -from rest_framework import serializers from django.utils import timezone -from team_production_system.models import ( - Mentor, - Availability, +from rest_framework import serializers + +from team_production_system.helpers import ( + is_overlapping_availabilities, + is_valid_end_time, + is_valid_start_time, ) +from team_production_system.models import Availability, Mentor +# V_1 API # # The mentor availability serializer class AvailabilitySerializer(serializers.ModelSerializer): - class Meta: model = Availability - fields = ('pk', 'mentor', 'start_time', 'end_time',) - read_only_fields = ('mentor', 'pk',) + fields = ( + 'pk', + 'mentor', + 'start_time', + 'end_time', + ) + read_only_fields = ( + 'mentor', + 'pk', + ) def create(self, validated_data): mentor = Mentor.objects.select_related('user').get( - user=self.context['request'].user) + user=self.context['request'].user + ) start_time = validated_data['start_time'] end_time = validated_data['end_time'] # Check if start time is between a start time and end time of # an existing availability - overlapping_condition1 = Availability.objects.filter( + overlapping_condition_1 = Availability.objects.filter( mentor=mentor, start_time__lt=start_time, end_time__gt=start_time, ).exists() # Check if end time is between a start time and end time of # an existing availability - overlapping_condition2 = Availability.objects.filter( - mentor=mentor, - start_time__lt=end_time, - end_time__gt=end_time + overlapping_condition_2 = Availability.objects.filter( + mentor=mentor, start_time__lt=end_time, end_time__gt=end_time ).exists() # Check if start time is between the new start_time and new end_time - overlapping_condition3 = Availability.objects.filter( - mentor=mentor, - start_time__gte=start_time, - start_time__lt=end_time + overlapping_condition_3 = Availability.objects.filter( + mentor=mentor, start_time__gte=start_time, start_time__lt=end_time ).exists() # Check if end time is between the new start_time and new end_time - overlapping_condition4 = Availability.objects.filter( - mentor=mentor, - end_time__gt=start_time, - end_time__lte=end_time + overlapping_condition_4 = Availability.objects.filter( + mentor=mentor, end_time__gt=start_time, end_time__lte=end_time ).exists() availability_overlap = ( - overlapping_condition1 - or overlapping_condition2 - or overlapping_condition3 - or overlapping_condition4) + overlapping_condition_1 + or overlapping_condition_2 + or overlapping_condition_3 + or overlapping_condition_4 + ) if not availability_overlap: - availability = Availability.objects.create( - mentor=mentor, **validated_data) + availability = Availability.objects.create(mentor=mentor, **validated_data) return availability - raise serializers.ValidationError( - "Input overlaps with existing availability.") + + raise serializers.ValidationError('Input overlaps with existing availability.') def validate(self, data): """ @@ -65,8 +71,8 @@ def validate(self, data): start_time = data['start_time'] end_time = data['end_time'] if start_time >= end_time: - raise serializers.ValidationError( - 'End time must be after start time.') + raise serializers.ValidationError('End time must be after start time.') + return data def validate_end_time(self, value): @@ -74,9 +80,35 @@ def validate_end_time(self, value): Check that the end_time is in the future. """ if value <= timezone.now(): + raise serializers.ValidationError('End time must be in the future.') + + return value + + +# V2 API # +# The mentor availability serializer +class AvailabilitySerializerV2(serializers.ModelSerializer): + class Meta: + model = Availability + fields = ['pk', 'mentor', 'start_time', 'end_time', 'status'] + read_only_fields = ('pk',) + + def validate(self, data): + mentor = Mentor.objects.select_related('user').get( + user=self.context['request'].user + ) + start_time = data.get('start_time') + end_time = data.get('end_time') + + if not is_valid_start_time(start_time): + raise serializers.ValidationError('Start time must be in the future.') + + if not is_valid_end_time(start_time, end_time): + raise serializers.ValidationError('End time must be after start time.') + + if is_overlapping_availabilities(mentor, data): raise serializers.ValidationError( - 'End time must be in the future.' + 'Availability overlaps with existing availability.' ) - return value - # TODO: Add validation for start times + return data diff --git a/team_production_system/serializers/custom_user.py b/team_production_system/serializers/custom_user.py index a19d055..3e3dd6c 100644 --- a/team_production_system/serializers/custom_user.py +++ b/team_production_system/serializers/custom_user.py @@ -1,4 +1,5 @@ from rest_framework import serializers + from team_production_system.models import CustomUser diff --git a/team_production_system/serializers/mentee_list.py b/team_production_system/serializers/mentee_list.py index f856df3..153fda0 100644 --- a/team_production_system/serializers/mentee_list.py +++ b/team_production_system/serializers/mentee_list.py @@ -1,7 +1,9 @@ from rest_framework import serializers + from team_production_system.models import CustomUser -from .mentee_profile import MenteeProfileSerializer + from .custom_user import CustomUserSerializer +from .mentee_profile import MenteeProfileSerializer class MenteeListSerializer(serializers.ModelSerializer): @@ -10,5 +12,12 @@ class MenteeListSerializer(serializers.ModelSerializer): class Meta: model = CustomUser - fields = ('user', 'pk', 'username', 'first_name', - 'last_name', 'is_mentee', 'mentee_profile') + fields = ( + 'user', + 'pk', + 'username', + 'first_name', + 'last_name', + 'is_mentee', + 'mentee_profile', + ) diff --git a/team_production_system/serializers/mentee_profile.py b/team_production_system/serializers/mentee_profile.py index 1022d6b..2464cbd 100644 --- a/team_production_system/serializers/mentee_profile.py +++ b/team_production_system/serializers/mentee_profile.py @@ -1,4 +1,5 @@ from rest_framework import serializers + from team_production_system.models import Mentee diff --git a/team_production_system/serializers/mentor_list.py b/team_production_system/serializers/mentor_list.py index f805dca..d107b3c 100644 --- a/team_production_system/serializers/mentor_list.py +++ b/team_production_system/serializers/mentor_list.py @@ -1,11 +1,9 @@ -from rest_framework import serializers from django.utils import timezone +from rest_framework import serializers + +from team_production_system.models import Availability, CustomUser, Mentor + from .availability import AvailabilitySerializer -from team_production_system.models import ( - CustomUser, - Mentor, - Availability, - ) # Serializer to show a list of all users flagged as a mentor @@ -18,9 +16,18 @@ class MentorListSerializer(serializers.ModelSerializer): class Meta: model = CustomUser - fields = ('pk', 'username', 'first_name', - 'last_name', 'profile_photo', 'is_mentor', 'about_me', - 'skills', 'availabilities', 'team_number') + fields = ( + 'pk', + 'username', + 'first_name', + 'last_name', + 'profile_photo', + 'is_mentor', + 'about_me', + 'skills', + 'availabilities', + 'team_number', + ) def get_about_me(self, obj): try: @@ -39,9 +46,9 @@ def get_skills(self, obj): def get_availabilities(self, obj): try: availabilities = Availability.objects.filter( - mentor=obj.pk, end_time__gte=timezone.now()) - serializer = AvailabilitySerializer( - instance=availabilities, many=True) + mentor=obj.pk, end_time__gte=timezone.now() + ) + serializer = AvailabilitySerializer(instance=availabilities, many=True) return serializer.data except Mentor.DoesNotExist: return None diff --git a/team_production_system/serializers/mentor_profile.py b/team_production_system/serializers/mentor_profile.py index e4c53fe..dd5d91d 100644 --- a/team_production_system/serializers/mentor_profile.py +++ b/team_production_system/serializers/mentor_profile.py @@ -1,11 +1,14 @@ -from rest_framework import serializers, fields -from .availability import AvailabilitySerializer +from rest_framework import fields, serializers + from team_production_system.models import Mentor +from .availability import AvailabilitySerializer + class MentorProfileSerializer(serializers.ModelSerializer): availabilities = AvailabilitySerializer( - many=True, read_only=True, source='mentor_availability') + many=True, read_only=True, source='mentor_availability' + ) skills = fields.MultipleChoiceField(choices=Mentor.SKILLS_CHOICES) team_number = serializers.IntegerField(required=False) diff --git a/team_production_system/serializers/notification_settings.py b/team_production_system/serializers/notification_settings.py index 38f7512..ae79ab7 100644 --- a/team_production_system/serializers/notification_settings.py +++ b/team_production_system/serializers/notification_settings.py @@ -1,11 +1,17 @@ from rest_framework import serializers + from team_production_system.models import NotificationSettings class NotificationSettingsSerializer(serializers.ModelSerializer): - class Meta: model = NotificationSettings - fields = ('pk', 'user', 'session_requested', 'session_confirmed', - 'session_canceled', 'fifteen_minute_alert', - 'sixty_minute_alert',) + fields = ( + 'pk', + 'user', + 'session_requested', + 'session_confirmed', + 'session_canceled', + 'fifteen_minute_alert', + 'sixty_minute_alert', + ) diff --git a/team_production_system/serializers/session.py b/team_production_system/serializers/session.py index c43eab0..d35d56c 100644 --- a/team_production_system/serializers/session.py +++ b/team_production_system/serializers/session.py @@ -1,22 +1,47 @@ from rest_framework import serializers + from team_production_system.models import Session class SessionSerializer(serializers.ModelSerializer): - mentor_first_name = serializers.SlugField(source='mentor.user.first_name', - read_only=True) - mentor_last_name = serializers.SlugField(source='mentor.user.last_name', - read_only=True) - mentee_first_name = serializers.SlugField(source='mentee.user.first_name', - read_only=True) - mentee_last_name = serializers.SlugField(source='mentee.user.last_name', - read_only=True) + mentor_first_name = serializers.SlugField( + source='mentor.user.first_name', + read_only=True, + ) + mentor_last_name = serializers.SlugField( + source='mentor.user.last_name', + read_only=True, + ) + mentee_first_name = serializers.SlugField( + source='mentee.user.first_name', + read_only=True, + ) + mentee_last_name = serializers.SlugField( + source='mentee.user.last_name', + read_only=True, + ) class Meta: model = Session - fields = ('pk', 'mentor', 'mentor_first_name', 'mentor_last_name', - 'mentor_availability', 'mentee', 'mentee_first_name', - 'mentee_last_name', 'start_time', 'end_time', 'status', - 'session_length',) - read_only_fields = ('mentor', 'mentor_first_name', 'mentor_last_name', - 'mentee', 'mentee_first_name', 'mentee_last_name') + fields = ( + 'pk', + 'mentor', + 'mentor_first_name', + 'mentor_last_name', + 'mentor_availability', + 'mentee', + 'mentee_first_name', + 'mentee_last_name', + 'start_time', + 'end_time', + 'status', + 'session_length', + ) + read_only_fields = ( + 'mentor', + 'mentor_first_name', + 'mentor_last_name', + 'mentee', + 'mentee_first_name', + 'mentee_last_name', + ) diff --git a/team_production_system/tasks.py b/team_production_system/tasks.py index 675fd38..eb4ef36 100644 --- a/team_production_system/tasks.py +++ b/team_production_system/tasks.py @@ -1,6 +1,8 @@ -from celery import shared_task from datetime import datetime, timedelta + +from celery import shared_task from django.utils import timezone + from .models import Session @@ -8,19 +10,22 @@ # Redis will run this every 5 minutes def notify(): now = datetime.now(timezone.utc) - # session = Session.objects.get(pk=session_pk) - sessions = Session.objects.filter(status="Confirmed") + sessions = Session.objects.filter(status='Confirmed') for session in sessions: start_time = session.start_time # We check a range of times to have Redis run every 5 minutes - if start_time - timedelta(minutes=60) \ - <= now \ - <= start_time - timedelta(minutes=55): + if ( + start_time - timedelta(minutes=60) + <= now + <= start_time - timedelta(minutes=55) + ): if session.mentor.user.notification_settings.sixty_minute_alert: session.sixty_min_notify() - elif start_time - timedelta(minutes=15) \ - <= now \ - <= start_time - timedelta(minutes=10): + elif ( + start_time - timedelta(minutes=15) + <= now + <= start_time - timedelta(minutes=10) + ): if session.mentor.user.notification_settings.fifteen_minute_alert: session.fifteen_min_notify() diff --git a/team_production_system/tests/end_to_end_tests/test_availability_list_create.py b/team_production_system/tests/end_to_end_tests/test_availability_list_create.py index 573c915..065b378 100644 --- a/team_production_system/tests/end_to_end_tests/test_availability_list_create.py +++ b/team_production_system/tests/end_to_end_tests/test_availability_list_create.py @@ -1,8 +1,9 @@ from django.urls import reverse from django.utils import timezone from rest_framework import status -from rest_framework.test import APITestCase, APIClient -from ...models import Availability, Mentor, CustomUser +from rest_framework.test import APIClient, APITestCase + +from ...models import Availability, CustomUser, Mentor from ...serializers import AvailabilitySerializer @@ -10,22 +11,20 @@ class AvailabilityListCreateTestCase(APITestCase): def setUp(self): # Create a Mentor object self.user = CustomUser.objects.create_user( - username='mentor', - email='mentor@example.com', - password='password' + username='mentor', email='mentor@example.com', password='password' ) self.mentor = Mentor.objects.create(user=self.user) # Create two Availability objects associated with the Mentor - self.availability1 = Availability.objects.create( + self.availability_1 = Availability.objects.create( mentor=self.mentor, start_time=timezone.now(), - end_time=timezone.now() + timezone.timedelta(hours=1) + end_time=timezone.now() + timezone.timedelta(hours=1), ) - self.availability2 = Availability.objects.create( + self.availability_2 = Availability.objects.create( mentor=self.mentor, start_time=timezone.now() + timezone.timedelta(days=1), - end_time=timezone.now() + timezone.timedelta(days=1, hours=1) + end_time=timezone.now() + timezone.timedelta(days=1, hours=1), ) # Create a Client self.client = APIClient() @@ -60,8 +59,7 @@ def test_get_availability_list(self): # Check that the response data matches the serialized Availability obj availabilities = Availability.objects.filter( - mentor=self.mentor, - end_time__gte=timezone.now() + mentor=self.mentor, end_time__gte=timezone.now() ).select_related('mentor__user') serializer = AvailabilitySerializer(availabilities, many=True) self.assertEqual(response.data, serializer.data) @@ -80,7 +78,7 @@ def test_create_availability(self): data = { 'mentor': self.mentor.pk, 'start_time': timezone.now() + timezone.timedelta(days=2), - 'end_time': timezone.now() + timezone.timedelta(days=2, hours=1) + 'end_time': timezone.now() + timezone.timedelta(days=2, hours=1), } url = reverse('availability') response = self.client.post(url, data, format='json') @@ -103,12 +101,14 @@ def test_create_availability_with_duplicate_start_time(self): # Authenticate as the Mentor self.client.force_authenticate(user=self.user) - '''Send a POST request to create a new Availability with - a start time that has already been used''' + """ + Send a POST request to create a new Availability with + a start time that has already been used. + """ data = { 'mentor': self.mentor.pk, - 'start_time': self.availability1.start_time, - 'end_time': timezone.now() + timezone.timedelta(hours=2) + 'start_time': self.availability_1.start_time, + 'end_time': timezone.now() + timezone.timedelta(hours=2), } url = reverse('availability') response = self.client.post(url, data, format='json') @@ -117,8 +117,7 @@ def test_create_availability_with_duplicate_start_time(self): self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) # Check that the response data contains an error message - self.assertEqual(response.data[0], - 'Input overlaps with existing availability.') + self.assertEqual(response.data[0], 'Input overlaps with existing availability.') def test_create_availability_with_duplicate_start_and_end_times(self): """ @@ -133,8 +132,8 @@ def test_create_availability_with_duplicate_start_and_end_times(self): # start and end times data = { 'mentor': self.mentor.pk, - 'start_time': self.availability1.start_time, - 'end_time': self.availability1.end_time + 'start_time': self.availability_1.start_time, + 'end_time': self.availability_1.end_time, } url = reverse('availability') response = self.client.post(url, data, format='json') @@ -143,8 +142,7 @@ def test_create_availability_with_duplicate_start_and_end_times(self): self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) # Check that the response data contains an error message - self.assertEqual(response.data[0], - 'Input overlaps with existing availability.') + self.assertEqual(response.data[0], 'Input overlaps with existing availability.') def test_create_availability_with_end_time_before_start_time(self): """ @@ -157,13 +155,13 @@ def test_create_availability_with_end_time_before_start_time(self): availability_data = { 'start_time': '2022-01-01T14:00:00Z', 'end_time': '2022-01-01T12:00:00Z', - 'mentor': self.mentor.pk + 'mentor': self.mentor.pk, } - response = self.client.post('/availability/', - availability_data, format='json') + response = self.client.post('/availability/', availability_data, format='json') self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(str(response.data['end_time'][0]), - 'End time must be in the future.') + self.assertEqual( + str(response.data['end_time'][0]), 'End time must be in the future.' + ) self.assertEqual(Availability.objects.count(), 2) def test_create_availability_inside_existing_availability(self): @@ -176,15 +174,14 @@ def test_create_availability_inside_existing_availability(self): self.client.force_authenticate(user=self.user) availability_data = { - 'start_time': self.availability1.start_time + - timezone.timedelta(minutes=15), - 'end_time': self.availability1.end_time - - timezone.timedelta(minutes=15), - 'mentor': self.mentor.pk + 'start_time': self.availability_1.start_time + + timezone.timedelta(minutes=15), + 'end_time': self.availability_1.end_time - timezone.timedelta(minutes=15), + 'mentor': self.mentor.pk, } - response = self.client.post('/availability/', - availability_data, format='json') + response = self.client.post('/availability/', availability_data, format='json') self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(str(response.data[0]), - 'Input overlaps with existing availability.') + self.assertEqual( + str(response.data[0]), 'Input overlaps with existing availability.' + ) self.assertEqual(Availability.objects.count(), 2) diff --git a/team_production_system/tests/end_to_end_tests/test_user_profile_patch_upload.py b/team_production_system/tests/end_to_end_tests/test_user_profile_patch_upload.py new file mode 100644 index 0000000..2ff1b42 --- /dev/null +++ b/team_production_system/tests/end_to_end_tests/test_user_profile_patch_upload.py @@ -0,0 +1,59 @@ +from django.core.files.uploadedfile import SimpleUploadedFile +from django.test import TestCase +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APIClient + +from team_production_system.models import CustomUser + + +class UserProfilePatchTestCase(TestCase): + def setUp(self): + self.client = APIClient() + self.user = CustomUser.objects.create( + username='baby_yoda', email='grogu@mandalor.edu', password='badpassword' + ) + + def test_new_user_gets_random_photo_assigned(self): + # Check that the user has a profile photo + self.assertIsNotNone(self.user.profile_photo) + + def test_profile_photo_uploaded_first_time(self): + # mock photo file + photo = SimpleUploadedFile( + 'photo.jpg', b'this is a photo', content_type='image/jpeg' + ) + # Make a PATCH request to update the user profile with the new photo + url = reverse('my-profile') + self.client.force_authenticate(user=self.user) + response = self.client.patch(url, {'profile_photo': photo}) + + # Check that the response status code is 200 OK + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Check that the profile photo was saved to the UserProfile object + self.user.refresh_from_db() + self.assertIsNotNone(self.user.profile_photo) + + def test_previous_file_deleted_when_new_photo_saved(self): + # get name of existing profile photo file + old_photo_name = self.user.profile_photo.name + # Create a new profile photo file + new_photo = SimpleUploadedFile( + 'new_photo.jpg', b'this is a new photo', content_type='image/jpeg' + ) + + # Make a PATCH request to update the user profile with the new photo + url = reverse('my-profile') + self.client.force_authenticate(user=self.user) + response = self.client.patch(url, {'profile_photo': new_photo}) + + # Check that the response status code is 200 OK + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Check that the new profile photo was saved + self.user.refresh_from_db() + self.assertNotEqual(self.user.profile_photo.name, old_photo_name) + + # Check that the previous profile photo file was deleted from storage + self.assertFalse(self.user.profile_photo.storage.exists(old_photo_name)) diff --git a/team_production_system/tests/test_availability.py b/team_production_system/tests/test_availability.py index 6d27159..c02ffe3 100644 --- a/team_production_system/tests/test_availability.py +++ b/team_production_system/tests/test_availability.py @@ -1,10 +1,12 @@ # flake8: noqa +import unittest +from datetime import timedelta + from django.test import TestCase from django.utils import timezone -from datetime import timedelta -from ..models import CustomUser, Mentor, Availability -import unittest + +from ..models import Availability, CustomUser, Mentor @unittest.skip("Test file is not ready yet") @@ -12,19 +14,15 @@ class AvailabilityTestCase(TestCase): def setUp(self): # Set up a mentor and an availability for testing self.mentor_user = CustomUser.objects.create_user( - username='mentor_user', - email='mentor_user@example.com', - password='password' + username='mentor_user', email='mentor_user@example.com', password='password' ) self.mentor = Mentor.objects.create( - user=self.mentor_user, - about_me='I am a mentor', - skills=['Python'] + user=self.mentor_user, about_me='I am a mentor', skills=['Python'] ) self.availability = Availability.objects.create( mentor=self.mentor, start_time=timezone.now(), - end_time=timezone.now() + timedelta(hours=1) + end_time=timezone.now() + timedelta(hours=1), ) def test_is_available(self): @@ -44,9 +42,7 @@ def test_get_next_seven_days_availability(self): slot_start_time = timezone.now() + timedelta(days=2) slot_end_time = slot_start_time + timedelta(hours=1) availability = Availability.objects.create( - mentor=self.mentor, - start_time=slot_start_time, - end_time=slot_end_time + mentor=self.mentor, start_time=slot_start_time, end_time=slot_end_time ) availabilities = self.availability.get_next_seven_days_availability() self.assertEqual(len(availabilities[2][1]), 1) @@ -63,6 +59,6 @@ def test_is_slot_available(self): mentor_availability=self.availability, mentee=self.mentee, start_time=start_time, - session_length=60 + session_length=60, ) self.assertFalse(self.availability.is_slot_available(start_time, end_time)) diff --git a/team_production_system/tests/unit_tests/models/test_availability_model.py b/team_production_system/tests/unit_tests/models/test_availability_model.py index 840766f..a4c2e61 100644 --- a/team_production_system/tests/unit_tests/models/test_availability_model.py +++ b/team_production_system/tests/unit_tests/models/test_availability_model.py @@ -1,15 +1,15 @@ -from django.test import TestCase from datetime import datetime, timedelta, timezone -from ....models import Availability, Mentor, CustomUser + +from django.test import TestCase + +from ....models import Availability, CustomUser, Mentor class AvailabilityTestCase(TestCase): def setUp(self): # Create a User and Mentor object self.user = CustomUser.objects.create_user( - username='mentor', - email='mentor@example.com', - password='password' + username='mentor', email='mentor@example.com', password='password' ) self.mentor = Mentor.objects.create(user=self.user) @@ -20,9 +20,8 @@ def test_create_availability(self): # Create an Availability object availability = Availability( - mentor=self.mentor, - start_time=start_time, - end_time=end_time) + mentor=self.mentor, start_time=start_time, end_time=end_time + ) # Save an Availability object associated with the Mentor availability.save() @@ -34,6 +33,7 @@ def test_create_availability(self): self.assertEqual(saved_availability.start_time, start_time) self.assertEqual(saved_availability.end_time, end_time) self.assertEqual(saved_availability.mentor, self.mentor) - self.assertEqual(str(saved_availability), - f"{self.mentor} is available from " - f"{start_time} to {end_time}.") + self.assertEqual( + str(saved_availability), + f'{self.mentor} is available from {start_time} to {end_time}.', + ) diff --git a/team_production_system/tests/unit_tests/models/test_mentor_model.py b/team_production_system/tests/unit_tests/models/test_mentor_model.py index 8f7fd34..31a35db 100644 --- a/team_production_system/tests/unit_tests/models/test_mentor_model.py +++ b/team_production_system/tests/unit_tests/models/test_mentor_model.py @@ -1,20 +1,21 @@ from django.test import TestCase -from ....models import Mentor, CustomUser +from ....models import CustomUser, Mentor -class MentorModelTest(TestCase): +class MentorModelTest(TestCase): def setUp(self): # Assuming CustomUser has a username and password - self.user = CustomUser.objects.create(username='testuser', - password='password123') + self.user = CustomUser.objects.create( + username='testuser', password='password123' + ) def test_mentor_creation(self): mentor = Mentor.objects.create( user=self.user, about_me='This is a test about me.', team_number=5, - skills=['HTML', 'CSS', 'Django'] + skills=['HTML', 'CSS', 'Django'], ) # Assert that the mentor object was saved and has the correct @@ -28,7 +29,7 @@ def test_str_representation(self): user=self.user, about_me='This is a test about me.', team_number=5, - skills=['HTML'] + skills=['HTML'], ) # Assert that the __str__ method returns the correct representation @@ -36,20 +37,14 @@ def test_str_representation(self): def test_default_about_me(self): # Create a Mentor object without specifying an 'about_me' - mentor = Mentor.objects.create( - user=self.user, - skills=['HTML'] - ) + mentor = Mentor.objects.create(user=self.user, skills=['HTML']) # Assert that the default value for 'about_me' is set self.assertEqual(mentor.about_me, '') def test_default_team_number(self): # Create a Mentor object without specifying a 'team_number' - mentor = Mentor.objects.create( - user=self.user, - skills=['HTML'] - ) + mentor = Mentor.objects.create(user=self.user, skills=['HTML']) # Assert that the default value for 'team_number' is set self.assertEqual(mentor.team_number, 0) diff --git a/team_production_system/tests/unit_tests/serializers/__init__.py b/team_production_system/tests/unit_tests/serializers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/team_production_system/tests/unit_tests/serializers/test_availability_serializer.py b/team_production_system/tests/unit_tests/serializers/test_availability_serializer.py new file mode 100644 index 0000000..5c61b82 --- /dev/null +++ b/team_production_system/tests/unit_tests/serializers/test_availability_serializer.py @@ -0,0 +1,146 @@ +from datetime import datetime + +from django.http import HttpRequest +from django.test import TestCase +from django.utils import timezone +from rest_framework import serializers + +from ....models import Availability, CustomUser, Mentor +from ....serializers import AvailabilitySerializer, AvailabilitySerializerV2 + + +class AvailabilitySerializerTestCase(TestCase): + def setUp(self): + self.user = CustomUser.objects.create_user( + username='testuser', password='testpass' + ) + self.mentor = Mentor.objects.create(user=self.user) + self.availability_attributes = { + 'mentor': self.mentor, + 'start_time': datetime.now(timezone.utc) + timezone.timedelta(hours=4), + 'end_time': datetime.now(timezone.utc) + timezone.timedelta(hours=5), + } + self.serializer_data = { + 'mentor': self.mentor.pk, + 'start_time': self.availability_attributes['start_time'], + 'end_time': self.availability_attributes['end_time'], + } + self.availability = Availability.objects.create(**self.availability_attributes) + self.request = HttpRequest() + self.request.user = self.user + self.serializer = AvailabilitySerializer( + instance=self.availability, context={'request': self.request} + ) + + def test_contains_expected_fields(self): + data = self.serializer.data + expected_keys = ['pk', 'mentor', 'start_time', 'end_time'] + self.assertCountEqual(data.keys(), expected_keys) + + def test_mentor_field_content(self): + data = self.serializer.data + self.assertEqual(data['mentor'], self.mentor.pk) + + def test_start_time_field_content(self): + data = self.serializer.data + self.assertEqual( + data['start_time'], + self.serializer_data['start_time'].strftime('%Y-%m-%dT%H:%M:%S.%fZ'), + ) + + def test_end_time_field_content(self): + data = self.serializer.data + self.assertEqual( + data['end_time'], + self.serializer_data['end_time'].strftime('%Y-%m-%dT%H:%M:%S.%fZ'), + ) + + def test_create(self): + start_time = datetime.now(timezone.utc) + timezone.timedelta(hours=5) + end_time = datetime.now(timezone.utc) + timezone.timedelta(hours=6) + serializer_data = { + 'mentor': self.mentor.pk, + 'start_time': start_time, + 'end_time': end_time, + } + serializer = AvailabilitySerializer( + data=serializer_data, context={'request': self.request} + ) + self.assertTrue(serializer.is_valid()) + availability = serializer.save() + self.assertEqual(availability.mentor, self.mentor) + self.assertEqual(availability.start_time, start_time) + self.assertEqual(availability.end_time, end_time) + + +# Test class for AvailabilitySerializerV2 +class AvailabilitySerializerV2TestCase(TestCase): + def setUp(self): + self.user = CustomUser.objects.create_user( + username='testuser', password='testpass' + ) + self.mentor = Mentor.objects.create(user=self.user) + self.request = HttpRequest() + self.request.user = self.user + self.availability_attributes = { + 'mentor': self.mentor, + 'start_time': timezone.now() + timezone.timedelta(hours=3), + 'end_time': timezone.now() + timezone.timedelta(hours=4), + } + self.serializer_data = { + 'mentor': self.mentor.pk, + 'start_time': self.availability_attributes['start_time'], + 'end_time': self.availability_attributes['end_time'], + } + self.availability = Availability.objects.create(**self.availability_attributes) + self.serializer = AvailabilitySerializerV2( + instance=self.availability, context={'request': self.request} + ) + + # Test that the serializer contains the expected fields + def test_contains_expected_fields(self): + data = self.serializer.data + expected_keys = {'pk', 'mentor', 'start_time', 'end_time', 'status'} + self.assertEqual(set(data.keys()), expected_keys) + + def test_create(self): + pass + + # Test validate method + def test_validate(self): + serializer = AvailabilitySerializerV2(context={'request': self.request}) + start_time = datetime.now(timezone.utc) + timezone.timedelta(hours=1) + end_time = timezone.now() + timezone.timedelta(hours=2) + data = {'start_time': start_time, 'end_time': end_time} + + # Test valid input data + validated_data = serializer.validate(data) + self.assertEqual(validated_data, data) + + # Test invalid start_time + start_time = timezone.now() - timezone.timedelta(hours=1) + end_time = timezone.now() + timezone.timedelta(hours=2) + data = { + 'start_time': start_time, + 'end_time': end_time, + } + with self.assertRaises(serializers.ValidationError) as context: + serializer.validate(data) + + self.assertEqual( + str(context.exception.detail[0]), 'Start time must be in the future.' + ) + + # Test invalid end_time + start_time = timezone.now() + timezone.timedelta(hours=2) + end_time = timezone.now() + timezone.timedelta(hours=1) + data = { + 'start_time': start_time, + 'end_time': end_time, + } + with self.assertRaises(serializers.ValidationError) as context: + serializer.validate(data) + + self.assertEqual( + str(context.exception.detail[0]), 'End time must be after start time.' + ) diff --git a/team_production_system/tests/unit_tests/tasks/test_tasks.py b/team_production_system/tests/unit_tests/tasks/test_tasks.py index 03fa5c7..e6780c3 100644 --- a/team_production_system/tests/unit_tests/tasks/test_tasks.py +++ b/team_production_system/tests/unit_tests/tasks/test_tasks.py @@ -1,7 +1,9 @@ -from django.test import TestCase -from django.utils import timezone from datetime import timedelta from unittest.mock import MagicMock + +from django.test import TestCase +from django.utils import timezone + from ....models import Session from ....tasks import notify diff --git a/team_production_system/tests/unit_tests/views/test_availability_create_list_view.py b/team_production_system/tests/unit_tests/views/test_availability_create_list_view.py index 976a213..6653c6b 100644 --- a/team_production_system/tests/unit_tests/views/test_availability_create_list_view.py +++ b/team_production_system/tests/unit_tests/views/test_availability_create_list_view.py @@ -1,28 +1,31 @@ +from datetime import timedelta +from unittest.mock import patch + +from django.test import TestCase from django.urls import reverse from django.utils import timezone from rest_framework import status from rest_framework.test import APIClient -from django.test import TestCase -from ....models import Mentor, Availability, CustomUser -from ....serializers import AvailabilitySerializer -from unittest.mock import patch + +from ....models import Availability, CustomUser, Mentor +from ....serializers import AvailabilitySerializer, AvailabilitySerializerV2 class AvailabilityListCreateViewTestCase(TestCase): def setUp(self): # Create a Mentor instance for the test self.user = CustomUser.objects.create_user( - username='testuser', - email='testuser@fake.com', - password='testpass') + username='testuser', email='testuser@fake.com', password='testpass' + ) self.mentor = Mentor.objects.create(user=self.user) # Create an Availability instance for the test self.availability = Availability.objects.create( mentor=self.mentor, start_time=timezone.now() + timezone.timedelta(hours=1), - end_time=timezone.now() + timezone.timedelta(hours=2) + end_time=timezone.now() + timezone.timedelta(hours=2), ) + self.url = reverse('availability') @patch('django.utils.timezone') def test_get_availability_list(self, mock_timezone): @@ -36,22 +39,23 @@ def test_get_availability_list(self, mock_timezone): self.client.force_authenticate(user=self.user) # Set up the mock timezone to return a fixed datetime - mock_timezone.now.return_value = timezone.datetime(2022, 1, 1, - tzinfo=timezone.utc) + mock_timezone.now.return_value = timezone.datetime( + 2022, 1, 1, tzinfo=timezone.utc + ) # Make a GET request to the AvailabilityList view response = self.client.get('/availability/', format='json') - # Check that the response status code is 200 OK self.assertEqual(response.status_code, 200) # Check that the response data contains the expected Availability self.assertEqual(len(response.data), 1) - self.assertEqual(response.data[0]['start_time'], - self.availability.start_time.isoformat().replace( - '+00:00', 'Z')) + self.assertEqual( + response.data[0]['start_time'], + self.availability.start_time.isoformat().replace('+00:00', 'Z'), + ) - def test_create_availability(self): + def test_create_availability_v1(self): """ Test that a POST request to create a new Availability returns a status code of 201 CREATED and the new Availability. @@ -64,11 +68,10 @@ def test_create_availability(self): data = { 'mentor': self.mentor.pk, 'start_time': timezone.now() + timezone.timedelta(hours=3), - 'end_time': timezone.now() + timezone.timedelta(hours=4) + 'end_time': timezone.now() + timezone.timedelta(hours=4), } - url = reverse('availability') - response = client.post(url, data, format='json') + response = client.post(self.url, data, format='json') # Check that the response has a status code of 201 CREATED self.assertEqual(response.status_code, status.HTTP_201_CREATED) @@ -76,10 +79,9 @@ def test_create_availability(self): # Check that the response data is the new Availability availability = Availability.objects.last() serializer = AvailabilitySerializer(availability) - self.assertEqual(response.data['start_time'], - serializer.data['start_time']) + self.assertEqual(response.data['start_time'], serializer.data['start_time']) - def test_create_availability_with_invalid_data(self): + def test_create_availability_v1_with_invalid_data(self): """ Test that a POST request to create a new Availability with invalid data returns a status code of 400 BAD REQUEST and an error message. @@ -92,14 +94,97 @@ def test_create_availability_with_invalid_data(self): data = { 'mentor': self.mentor.pk, 'start_time': timezone.now() + timezone.timedelta(hours=2), - 'end_time': timezone.now() + timezone.timedelta(hours=1) + 'end_time': timezone.now() + timezone.timedelta(hours=1), } - url = reverse('availability') - response = client.post(url, data, format='json') + + response = client.post(self.url, data, format='json') # Check that the response has a status code of 400 BAD REQUEST self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) # Check that the response data contains an error message - self.assertEqual(response.data['non_field_errors'][0], - 'End time must be after start time.') + self.assertEqual(str(response.data[0]), 'End time must be after start time.') + + def test_create_availability_v2(self): + self.client = APIClient() + self.client.force_authenticate(user=self.user) + + start_time = timezone.now() + timedelta(hours=5) + end_time = start_time + timedelta(hours=1) + data = { + 'start_time': start_time, + 'end_time': end_time, + } + headers = {'HTTP_ACCEPT': 'application/json; version=v2'} + response = self.client.post(self.url, data, format='json', **headers) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Availability.objects.count(), 3) + availability = Availability.objects.last() + self.assertEqual(availability.start_time, end_time - timedelta(minutes=30)) + self.assertEqual(availability.end_time, end_time) + self.assertEqual(availability.mentor, self.mentor) + + def test_create_availability_v2_with_overlap(self): + self.client = APIClient() + self.client.force_authenticate(user=self.user) + + start_time = timezone.now() + timedelta(hours=7) + end_time = start_time + timedelta(hours=8) + Availability.objects.create( + mentor=self.mentor, start_time=start_time, end_time=end_time + ) + data = { + 'start_time': start_time, + 'end_time': end_time, + } + headers = {'HTTP_ACCEPT': 'application/json; version=v2'} + response = self.client.post(self.url, data, format='json', **headers) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(Availability.objects.count(), 2) + + def test_create_availability_v2_with_past_end_time(self): + self.client = APIClient() + self.client.force_authenticate(user=self.user) + + start_time = timezone.now() + timedelta(hours=1) + end_time = timezone.now() - timedelta(hours=1) + data = { + 'start_time': start_time, + 'end_time': end_time, + } + + response = self.client.post(self.url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(Availability.objects.count(), 1) + + def test_create_availability_v2_with_time_not_divisible_by_30(self): + self.client = APIClient() + self.client.force_authenticate(user=self.user) + + start_time = timezone.now() + timedelta(hours=2) + end_time = timezone.now() + timedelta(hours=3, minutes=15) + data = { + 'start_time': start_time, + 'end_time': end_time, + } + headers = {'HTTP_ACCEPT': 'application/json; version=v2'} + response = self.client.post(self.url, data, format='json', **headers) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(len(response.data), 2) + availability_1 = Availability.objects.get(pk=response.data[0]['pk']) + availability_2 = Availability.objects.get(pk=response.data[1]['pk']) + self.assertEqual(availability_1.end_time, start_time + timedelta(minutes=30)) + self.assertNotEqual(availability_2.end_time, end_time) + self.assertEqual(availability_1.mentor, self.mentor) + + def test_list_availabilities_v2(self): + self.client = APIClient() + self.client.force_authenticate(user=self.user) + + headers = {'HTTP_ACCEPT': 'application/json; version=v2'} + response = self.client.get(self.url, format='json', **headers) + self.assertEqual(response.status_code, status.HTTP_200_OK) + availabilities = Availability.objects.filter(mentor=self.mentor) + serializer = AvailabilitySerializerV2(availabilities, many=True) + self.assertEqual(response.data, serializer.data) diff --git a/team_production_system/tests/unit_tests/views/test_availability_delete_view.py b/team_production_system/tests/unit_tests/views/test_availability_delete_view.py new file mode 100644 index 0000000..8483f15 --- /dev/null +++ b/team_production_system/tests/unit_tests/views/test_availability_delete_view.py @@ -0,0 +1,34 @@ +from django.test import TestCase +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APIClient + +from ....models import Availability, CustomUser, Mentor + + +class AvailabilityDeleteViewTestCase(TestCase): + def setUp(self): + self.client = APIClient() + self.user = CustomUser.objects.create_user( + username='testuser', password='testpass' + ) + self.mentor = Mentor.objects.create(user=self.user) + self.client.force_authenticate(user=self.user) + + def test_get_object(self): + # Create an Availability instance for the logged-in user + availability = Availability.objects.create( + mentor=self.mentor, + start_time='2022-01-01T00:00:00Z', + end_time='2022-01-01T01:00:00Z', + ) + + # Test that get_object returns the correct Availability instance + url = reverse('availability-delete', args=[availability.id]) + response = self.client.delete(url) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + # Test that get_object raises Http404 for non-existent Availability + url = reverse('availability-delete', args=[availability.id + 1]) + response = self.client.delete(url) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) diff --git a/team_production_system/urls.py b/team_production_system/urls.py index 0bb5129..87f22df 100644 --- a/team_production_system/urls.py +++ b/team_production_system/urls.py @@ -1,47 +1,58 @@ from django.urls import path -from team_production_system import views +from team_production_system import views urlpatterns = [ - # User information end points - path('myprofile/', views.UserProfile.as_view(), name='my-profile'), - - # List of mentor end points - path('mentor/', views.MentorList.as_view(), name='mentor-list'), - path('mentorinfo/', views.MentorInfoView.as_view(), name='mentor-info'), - path('mentor//', views.MentorFilteredList.as_view(), - name='mentor-filtered-list'), - path('mentorinfoupdate/', views.MentorInfoUpdateView.as_view(), - name='mentor-info-update'), - - - # List of mentee end points - path('mentee/', views.MenteeList.as_view(), name='mentee-list'), - path('menteeinfo/', views.MenteeInfoView.as_view(), name='mentee-info'), - path('menteeinfoupdate/', views.MenteeInfoUpdateView.as_view(), - name='mentee-info-update'), - - # End points related to sessions - path( - 'availability/', - views.AvailabilityListCreateView.as_view(), - name='availability' - ), - # Delete availability - path('availability//', views.AvailabilityDeleteView.as_view(), - name='availability-delete'), - path('session/', views.SessionView.as_view(), name='session'), - path('archivesession/', views.ArchiveSessionView.as_view(), - name='archive-session'), - path('sessionrequest/', views.SessionRequestView.as_view(), - name='session'), - path('sessionrequest//', views.SessionRequestDetailView.as_view(), - name='session-detail'), - path('sessionsignuplist/', views.SessionSignupListView.as_view(), - name='session-signup-list'), - path( - 'notificationsettings//', - views.NotificationSettingsView.as_view(), - name='notification-settings' - ), + # User endpoints + path('myprofile/', views.UserProfile.as_view(), name='my-profile'), + # Mentor end points + path('mentor/', views.MentorList.as_view(), name='mentor-list'), + path('mentorinfo/', views.MentorInfoView.as_view(), name='mentor-info'), + path( + 'mentor//', + views.MentorFilteredList.as_view(), + name='mentor-filtered-list', + ), + path( + 'mentorinfoupdate/', + views.MentorInfoUpdateView.as_view(), + name='mentor-info-update', + ), + # Mentee endpoints + path('mentee/', views.MenteeList.as_view(), name='mentee-list'), + path('menteeinfo/', views.MenteeInfoView.as_view(), name='mentee-info'), + path( + 'menteeinfoupdate/', + views.MenteeInfoUpdateView.as_view(), + name='mentee-info-update', + ), + # Availability endpoints + path( + 'availability/', views.AvailabilityListCreateView.as_view(), name='availability' + ), + path( + 'availability//', + views.AvailabilityDeleteView.as_view(), + name='availability-delete', + ), + # Session endpoints + path('session/', views.SessionView.as_view(), name='session'), + path('archivesession/', views.ArchiveSessionView.as_view(), name='archive-session'), + path('sessionrequest/', views.SessionRequestView.as_view(), name='session'), + path( + 'sessionrequest//', + views.SessionRequestDetailView.as_view(), + name='session-detail', + ), + path( + 'sessionsignuplist/', + views.SessionSignUpListView.as_view(), + name='session-signup-list', + ), + # Notification endpoints + path( + 'notificationsettings//', + views.NotificationSettingsView.as_view(), + name='notification-settings', + ), ] diff --git a/team_production_system/views/__init__.py b/team_production_system/views/__init__.py index 59cc71b..39472b4 100644 --- a/team_production_system/views/__init__.py +++ b/team_production_system/views/__init__.py @@ -14,5 +14,5 @@ from .session_list import SessionView from .session_request_detail import SessionRequestDetailView from .session_request_list_create import SessionRequestView -from .session_signup_list import SessionSignupListView +from .session_signup_list import SessionSignUpListView from .user_profile import UserProfile diff --git a/team_production_system/views/archive_session_list.py b/team_production_system/views/archive_session_list.py index 4fc016c..48bdeb0 100644 --- a/team_production_system/views/archive_session_list.py +++ b/team_production_system/views/archive_session_list.py @@ -1,9 +1,10 @@ +from django.db.models import Q +from django.utils import timezone from rest_framework import generics +from rest_framework.permissions import IsAuthenticated + from team_production_system.models import Session from team_production_system.serializers import SessionSerializer -from rest_framework.permissions import IsAuthenticated -from django.utils import timezone -from django.db.models import Q class ArchiveSessionView(generics.ListAPIView): @@ -13,6 +14,8 @@ class ArchiveSessionView(generics.ListAPIView): def get_queryset(self): # Get sessions for the logged in user - return Session.objects.filter(Q(mentor__user=self.request.user) | - Q(mentee__user=self.request.user), - end_time__lt=timezone.now()) + return Session.objects.filter( + Q(mentor__user=self.request.user) | Q( + mentee__user=self.request.user), + start_time__lt=timezone.now(), + ) diff --git a/team_production_system/views/availability_delete.py b/team_production_system/views/availability_delete.py index 77de3e8..1fca5ce 100644 --- a/team_production_system/views/availability_delete.py +++ b/team_production_system/views/availability_delete.py @@ -1,9 +1,10 @@ +from django.http import Http404 from rest_framework import generics from rest_framework.permissions import IsAuthenticated -from django.http import Http404 -from team_production_system.serializers import AvailabilitySerializer -from team_production_system.models import Availability + from team_production_system.custom_permissions import IsOwnerOrAdmin +from team_production_system.models import Availability +from team_production_system.serializers import AvailabilitySerializer class AvailabilityDeleteView(generics.DestroyAPIView): @@ -13,9 +14,10 @@ class AvailabilityDeleteView(generics.DestroyAPIView): def get_object(self): try: # Get the Availability instance for the logged in user - availability = Availability.objects.select_related( - 'mentor__user').get(id=self.kwargs['pk']) + availability = Availability.objects.select_related('mentor__user').get( + id=self.kwargs['pk'] + ) self.check_object_permissions(self.request, availability) return availability except Availability.DoesNotExist: - raise Http404("No Availability matches the given query.") + raise Http404('No Availability matches the given query.') diff --git a/team_production_system/views/availability_list_create.py b/team_production_system/views/availability_list_create.py index 2b73a0a..02eab3b 100644 --- a/team_production_system/views/availability_list_create.py +++ b/team_production_system/views/availability_list_create.py @@ -1,8 +1,14 @@ -from rest_framework import generics -from rest_framework.permissions import IsAuthenticated from django.utils import timezone -from team_production_system.serializers import AvailabilitySerializer -from team_production_system.models import Mentor, Availability +from rest_framework import generics, status +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from team_production_system.helpers import create_30_min_availabilities +from team_production_system.models import Availability, Mentor +from team_production_system.serializers import ( + AvailabilitySerializer, + AvailabilitySerializerV2, +) # Create and view all availabilities @@ -10,11 +16,64 @@ class AvailabilityListCreateView(generics.ListCreateAPIView): serializer_class = AvailabilitySerializer permission_classes = [IsAuthenticated] + def handle_exception(self, exc): + response = super().handle_exception(exc) + + if isinstance(response.data, dict) and 'non_field_errors' in response.data: + response.data = response.data['non_field_errors'] + + return response + def get_queryset(self): - # Get the Mentor instance for the logged in user - mentor = Mentor.objects.get(user=self.request.user) - # Exclude any availability that has an end time in the past - # and filter availabilities belonging to the logged in user's mentor - return Availability.objects.filter(mentor=mentor, - end_time__gte=timezone.now() - ).select_related('mentor__user') + if self.request.version == 'v2': + # Get the Mentor instance for the logged in user + mentor = Mentor.objects.select_related('user').get(user=self.request.user) + return ( + Availability.objects.filter(mentor=mentor, end_time__gte=timezone.now()) + .select_related('mentor__user') + .order_by('start_time') + ) + else: + # Get the Mentor instance for the logged in user + mentor = Mentor.objects.get(user=self.request.user) + # Exclude any availability that has an end time in the past + # and filter availabilities belonging to the logged in mentor + return Availability.objects.filter( + mentor=mentor, end_time__gte=timezone.now() + ).select_related('mentor__user') + + def create(self, request, *args, **kwargs): + if request.version == 'v2': + # Validate data before creating 30 min availabilities + request.data.update(mentor=request.user.mentor) + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + # Create 30 min availabilities + availabilities = create_30_min_availabilities( + request.data['start_time'], + request.data['end_time'], + request.user.mentor, + ) + # Serialize and save 30 min availabilities + serializer = self.get_serializer(data=availabilities, many=True) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + + headers = self.get_success_headers(serializer.data) + return Response( + serializer.data, status=status.HTTP_201_CREATED, headers=headers + ) + + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + headers = self.get_success_headers(serializer.data) + return Response( + serializer.data, status=status.HTTP_201_CREATED, headers=headers + ) + + def get_serializer_class(self): + if self.request.version == 'v2': + return AvailabilitySerializerV2 + else: + return AvailabilitySerializer diff --git a/team_production_system/views/mentee_info_list_create.py b/team_production_system/views/mentee_info_list_create.py index 570d17e..e4df351 100644 --- a/team_production_system/views/mentee_info_list_create.py +++ b/team_production_system/views/mentee_info_list_create.py @@ -1,7 +1,8 @@ from rest_framework import generics from rest_framework.permissions import IsAuthenticated -from team_production_system.serializers import MenteeProfileSerializer + from team_production_system.models import Mentee +from team_production_system.serializers import MenteeProfileSerializer # View to allow mentees to create and view the about me/skills. diff --git a/team_production_system/views/mentee_info_update.py b/team_production_system/views/mentee_info_update.py index 991d291..e23fd33 100644 --- a/team_production_system/views/mentee_info_update.py +++ b/team_production_system/views/mentee_info_update.py @@ -1,5 +1,6 @@ from rest_framework import generics from rest_framework.permissions import IsAuthenticated + from team_production_system.serializers import MenteeProfileSerializer diff --git a/team_production_system/views/mentee_list.py b/team_production_system/views/mentee_list.py index 1094c18..f23436b 100644 --- a/team_production_system/views/mentee_list.py +++ b/team_production_system/views/mentee_list.py @@ -1,8 +1,9 @@ from rest_framework import generics, status -from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated -from team_production_system.serializers import MenteeListSerializer +from rest_framework.response import Response + from team_production_system.models import CustomUser +from team_production_system.serializers import MenteeListSerializer class MenteeList(generics.ListAPIView): @@ -10,15 +11,19 @@ class MenteeList(generics.ListAPIView): def get(self, request, *args, **kwargs): try: - queryset = CustomUser.objects.filter( - is_mentee=True).select_related("mentee") + queryset = CustomUser.objects.filter(is_mentee=True).select_related( + 'mentee' + ) except Exception: - return Response({"error": "Failed to retrieve mentee list."}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return Response( + {'error': 'Failed to retrieve mentee list.'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) if not queryset.exists(): - return Response({"message": "No mentees found."}, - status=status.HTTP_404_NOT_FOUND) + return Response( + {'message': 'No mentees found.'}, status=status.HTTP_404_NOT_FOUND + ) serializer = MenteeListSerializer(queryset, many=True) response_data = serializer.data diff --git a/team_production_system/views/mentor_filtered_list.py b/team_production_system/views/mentor_filtered_list.py index 8ed016b..ac519b1 100644 --- a/team_production_system/views/mentor_filtered_list.py +++ b/team_production_system/views/mentor_filtered_list.py @@ -1,8 +1,9 @@ from rest_framework import generics, status -from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated -from team_production_system.serializers import MentorProfileSerializer +from rest_framework.response import Response + from team_production_system.models import Mentor +from team_production_system.serializers import MentorProfileSerializer class MentorFilteredList(generics.ListAPIView): @@ -15,8 +16,9 @@ def get(self, request, *args, **kwargs): queryset = queryset.filter(skills__icontains=skill) if not queryset.exists(): - return Response({"message": "No mentors found."}, - status=status.HTTP_404_NOT_FOUND) + return Response( + {'message': 'No mentors found.'}, status=status.HTTP_404_NOT_FOUND + ) serializer = MentorProfileSerializer(queryset, many=True) return Response(serializer.data) diff --git a/team_production_system/views/mentor_info_list_create.py b/team_production_system/views/mentor_info_list_create.py index 3ead109..f7ac5a1 100644 --- a/team_production_system/views/mentor_info_list_create.py +++ b/team_production_system/views/mentor_info_list_create.py @@ -1,7 +1,8 @@ from rest_framework import generics from rest_framework.permissions import IsAuthenticated -from team_production_system.serializers import MentorProfileSerializer + from team_production_system.models import Mentor +from team_production_system.serializers import MentorProfileSerializer # View to allow mentors to create and view the about me/skills. diff --git a/team_production_system/views/mentor_info_update.py b/team_production_system/views/mentor_info_update.py index 10cb82f..fa6791e 100644 --- a/team_production_system/views/mentor_info_update.py +++ b/team_production_system/views/mentor_info_update.py @@ -1,5 +1,6 @@ from rest_framework import generics from rest_framework.permissions import IsAuthenticated + from team_production_system.serializers import MentorProfileSerializer diff --git a/team_production_system/views/mentor_list.py b/team_production_system/views/mentor_list.py index 11aec69..3c61a95 100644 --- a/team_production_system/views/mentor_list.py +++ b/team_production_system/views/mentor_list.py @@ -1,28 +1,29 @@ from rest_framework import generics, status -from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated -from team_production_system.serializers import MentorListSerializer +from rest_framework.response import Response + from team_production_system.models import CustomUser +from team_production_system.serializers import MentorListSerializer # View to see a list of all users flagged as a mentor class MentorList(generics.ListAPIView): permission_classes = [IsAuthenticated] - queryset = CustomUser.objects.filter( - is_mentor=True - ).select_related( - "mentor" - ).prefetch_related( - "mentor__mentor_availability") + queryset = ( + CustomUser.objects.filter(is_mentor=True) + .select_related('mentor') + .prefetch_related('mentor__mentor_availability') + ) serializer_class = MentorListSerializer def list(self, request, *args, **kwargs): queryset = self.get_queryset() if not queryset: - return Response({"message": "No mentors found."}, - status=status.HTTP_404_NOT_FOUND) + return Response( + {'message': 'No mentors found.'}, status=status.HTTP_404_NOT_FOUND + ) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) diff --git a/team_production_system/views/notification_settings.py b/team_production_system/views/notification_settings.py index 0e1d3eb..8e41d1f 100644 --- a/team_production_system/views/notification_settings.py +++ b/team_production_system/views/notification_settings.py @@ -1,9 +1,8 @@ from rest_framework import generics -from team_production_system.custom_permissions import ( - NotificationSettingsPermission -) -from team_production_system.serializers import NotificationSettingsSerializer + +from team_production_system.custom_permissions import NotificationSettingsPermission from team_production_system.models import NotificationSettings +from team_production_system.serializers import NotificationSettingsSerializer class NotificationSettingsView(generics.RetrieveUpdateAPIView): diff --git a/team_production_system/views/session_list.py b/team_production_system/views/session_list.py index db5ec69..4606556 100644 --- a/team_production_system/views/session_list.py +++ b/team_production_system/views/session_list.py @@ -1,9 +1,10 @@ +from django.db.models import Q +from django.utils import timezone from rest_framework import generics from rest_framework.permissions import IsAuthenticated -from django.utils import timezone -from django.db.models import Q -from team_production_system.serializers import SessionSerializer + from team_production_system.models import Session +from team_production_system.serializers import SessionSerializer class SessionView(generics.ListAPIView): @@ -15,7 +16,7 @@ def get_queryset(self): # Get sessions for the logged in user # Exclude sessions that have already ended and # order by sessions that are coming up next first. - return Session.objects.filter(Q(mentor__user=self.request.user) | - Q(mentee__user=self.request.user), - start_time__gt=timezone.now() - ).order_by('start_time') + return Session.objects.filter( + Q(mentor__user=self.request.user) | Q(mentee__user=self.request.user), + start_time__gt=timezone.now(), + ).order_by('start_time') diff --git a/team_production_system/views/session_request_detail.py b/team_production_system/views/session_request_detail.py index 6241533..bc5072a 100644 --- a/team_production_system/views/session_request_detail.py +++ b/team_production_system/views/session_request_detail.py @@ -1,7 +1,8 @@ from rest_framework import generics + from team_production_system.custom_permissions import IsMentorMentee -from team_production_system.serializers import SessionSerializer from team_production_system.models import Session +from team_production_system.serializers import SessionSerializer class SessionRequestDetailView(generics.RetrieveUpdateDestroyAPIView): @@ -20,14 +21,18 @@ def perform_update(self, serializer): session.save() # If mentee cancels session, check mentor notification - if self.request.user.is_mentee and \ - session.mentor.user.notification_settings.session_canceled: + if ( + self.request.user.is_mentee + and session.mentor.user.notification_settings.session_canceled + ): session.mentor_cancel_notify() # If mentor cancels session, check mentee notification - elif self.request.user.is_mentor and \ - session.mentee.user.notification_settings.session_canceled: + elif ( + self.request.user.is_mentor + and session.mentee.user.notification_settings.session_canceled + ): session.mentee_cancel_notify() # Notify mentee when a mentor confirms session request diff --git a/team_production_system/views/session_request_list_create.py b/team_production_system/views/session_request_list_create.py index 371afa6..52e4e8a 100644 --- a/team_production_system/views/session_request_list_create.py +++ b/team_production_system/views/session_request_list_create.py @@ -1,25 +1,27 @@ -from rest_framework import generics -from rest_framework.permissions import IsAuthenticated +from datetime import datetime, timedelta + from django.core.exceptions import ValidationError from django.db.models import Q -from datetime import datetime, timedelta from django.utils import timezone +from rest_framework import generics +from rest_framework.permissions import IsAuthenticated + +from team_production_system.models import Availability, Mentee, Session from team_production_system.serializers import SessionSerializer -from team_production_system.models import (Availability, - Mentee, - Session) def time_convert(time, minutes): # Convert string from front end to datetime object - datetime_obj = datetime.strptime( - time, '%Y-%m-%dT%H:%M:%S.%fZ').replace(tzinfo=timezone.utc) + datetime_obj = datetime.strptime(time, '%Y-%m-%dT%H:%M:%S.%fZ').replace( + tzinfo=timezone.utc + ) # Change time dependent on session length minutes datetime_delta = datetime_obj - timedelta(minutes=minutes) # Convert datetime object back to string - new_start_time = datetime.strftime( - datetime_delta, '%Y-%m-%d %H:%M:%S%z')[:-2] + new_start_time = datetime.strftime(datetime_delta, '%Y-%m-%d %H:%M:%S%z')[:-2] return new_start_time + + # Time conversion helper function # During a session request, must convert start_time string to a datetime # object in order to use timedelta to check for overlapping sessions @@ -36,8 +38,7 @@ def perform_create(self, serializer): mentor_availability_id = self.request.data.get('mentor_availability') # Get the mentor availability instance - mentor_availability = Availability.objects.get( - id=mentor_availability_id) + mentor_availability = Availability.objects.get(id=mentor_availability_id) # Ensure no overlap between mentor or mentee's sessions mentor = mentor_availability.mentor @@ -51,32 +52,50 @@ def perform_create(self, serializer): new_start_time = time_convert(start_time, session_length) if Session.objects.filter( - Q(mentor=mentor, start_time=start_time, - status__in=['Pending', 'Confirmed']) | - Q(mentor=mentor, start_time=new_start_time, - session_length=60, status__in=['Pending', 'Confirmed']) + Q( + mentor=mentor, + start_time=start_time, + status__in=['Pending', 'Confirmed'], + ) + | Q( + mentor=mentor, + start_time=new_start_time, + session_length=60, + status__in=['Pending', 'Confirmed'], + ) ).exists(): raise ValidationError( 'A session with this mentor is \ - already scheduled during this time.') + already scheduled during this time.' + ) elif Session.objects.filter( - Q(mentee=mentee, start_time=start_time, - status__in=['Pending', 'Confirmed']) | - Q(mentee=mentee, start_time=new_start_time, - session_length=60, status__in=['Pending', 'Confirmed']) + Q( + mentee=mentee, + start_time=start_time, + status__in=['Pending', 'Confirmed'], + ) + | Q( + mentee=mentee, + start_time=new_start_time, + session_length=60, + status__in=['Pending', 'Confirmed'], + ) ).exists(): raise ValidationError( 'A session with this mentee is \ - already scheduled during this time.') + already scheduled during this time.' + ) # Set the mentor for the session else: - serializer.save(mentor=mentor, - mentor_availability=mentor_availability, - mentee=mentee) + serializer.save( + mentor=mentor, + mentor_availability=mentor_availability, + mentee=mentee, + ) - # Email notification to the mentor + # Email notification to the mentor session = serializer.instance if session.mentor.user.notification_settings.session_requested: session.mentor_session_notify() @@ -86,34 +105,58 @@ def perform_create(self, serializer): after_start_time = time_convert(start_time, -30) if Session.objects.filter( - Q(mentor=mentor, start_time=start_time, - status__in=['Pending', 'Confirmed']) | - Q(mentor=mentor, start_time=before_start_time, - session_length=60, status__in=['Pending', 'Confirmed']) | - Q(mentor=mentor, start_time=after_start_time, - status__in=['Pending', 'Confirmed']) + Q( + mentor=mentor, + start_time=start_time, + status__in=['Pending', 'Confirmed'], + ) + | Q( + mentor=mentor, + start_time=before_start_time, + session_length=60, + status__in=['Pending', 'Confirmed'], + ) + | Q( + mentor=mentor, + start_time=after_start_time, + status__in=['Pending', 'Confirmed'], + ) ).exists(): raise ValidationError( 'A session with this mentor is \ - already scheduled during this time.') + already scheduled during this time.' + ) elif Session.objects.filter( - Q(mentee=mentee, start_time=start_time, - status__in=['Pending', 'Confirmed']) | - Q(mentee=mentee, start_time=before_start_time, - session_length=60, status__in=['Pending', 'Confirmed']) | - Q(mentee=mentee, start_time=after_start_time, - status__in=['Pending', 'Confirmed']) + Q( + mentee=mentee, + start_time=start_time, + status__in=['Pending', 'Confirmed'], + ) + | Q( + mentee=mentee, + start_time=before_start_time, + session_length=60, + status__in=['Pending', 'Confirmed'], + ) + | Q( + mentee=mentee, + start_time=after_start_time, + status__in=['Pending', 'Confirmed'], + ) ).exists(): raise ValidationError( 'A session with this mentee is already \ - scheduled during this time.') + scheduled during this time.' + ) # Set the mentor for the session else: - serializer.save(mentor=mentor, - mentor_availability=mentor_availability, - mentee=mentee) + serializer.save( + mentor=mentor, + mentor_availability=mentor_availability, + mentee=mentee, + ) # Email notification to the mentor session = serializer.instance diff --git a/team_production_system/views/session_signup_list.py b/team_production_system/views/session_signup_list.py index 3f70db1..6fbb7c1 100644 --- a/team_production_system/views/session_signup_list.py +++ b/team_production_system/views/session_signup_list.py @@ -1,19 +1,21 @@ -from rest_framework import generics from datetime import timedelta + from django.utils import timezone +from rest_framework import generics from rest_framework.permissions import IsAuthenticated -from team_production_system.serializers import SessionSerializer + from team_production_system.models import Session +from team_production_system.serializers import SessionSerializer -# View to show mentor timeslots a mentee can sign up for -class SessionSignupListView(generics.ListAPIView): +# View to show mentor time slots a mentee can sign up for +class SessionSignUpListView(generics.ListAPIView): queryset = Session.objects.all() serializer_class = SessionSerializer permission_classes = [IsAuthenticated] def get_queryset(self): # Filter out completed sessions - return Session.objects.exclude(status='Completed', - start_time__lt=timezone.now() - - timedelta(hours=24)) + return Session.objects.exclude( + status='Completed', start_time__lt=timezone.now() - timedelta(hours=24) + ) diff --git a/team_production_system/views/user_profile.py b/team_production_system/views/user_profile.py index 2a855af..7bd28d3 100644 --- a/team_production_system/views/user_profile.py +++ b/team_production_system/views/user_profile.py @@ -1,11 +1,10 @@ from rest_framework import generics, status -from rest_framework.response import Response from rest_framework.parsers import MultiPartParser from rest_framework.permissions import IsAuthenticated -from django.conf import settings -import boto3 -from team_production_system.serializers import CustomUserSerializer +from rest_framework.response import Response + from team_production_system.models import CustomUser +from team_production_system.serializers import CustomUserSerializer # View to update the user profile information @@ -18,23 +17,35 @@ class UserProfile(generics.RetrieveUpdateDestroyAPIView): def get_object(self): user = self.request.user if not user.is_authenticated: - return Response({'error': 'User is not authenticated.'}, - status=status.HTTP_401_UNAUTHORIZED) + return Response( + {'error': 'User is not authenticated.'}, + status=status.HTTP_401_UNAUTHORIZED, + ) + try: return user except CustomUser.DoesNotExist: - return Response({'error': 'User not found.'}, - status=status.HTTP_404_NOT_FOUND) + return Response( + {'error': 'User not found.'}, status=status.HTTP_404_NOT_FOUND + ) except Exception: - return Response({ - 'error': 'An unexpected error occurred.' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return Response( + {'error': 'An unexpected error occurred.'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) def patch(self, request, *args, **kwargs): user = self.request.user - fields = ['first_name', 'last_name', 'email', - 'phone_number', 'is_mentor', 'is_mentee', 'is_active'] + fields = [ + 'first_name', + 'last_name', + 'email', + 'phone_number', + 'is_mentor', + 'is_mentee', + 'is_active', + ] for field in fields: if field in request.data: @@ -42,10 +53,8 @@ def patch(self, request, *args, **kwargs): if 'profile_photo' in request.FILES: if user.profile_photo: - s3 = boto3.client('s3') - s3.delete_object( - Bucket=settings.AWS_STORAGE_BUCKET_NAME, - Key=user.profile_photo.name) + # remove the file from storage + user.profile_photo.storage.delete(user.profile_photo.name) user.profile_photo = request.FILES['profile_photo'] diff --git a/whitelist.txt b/whitelist.txt new file mode 100644 index 0000000..265b15c --- /dev/null +++ b/whitelist.txt @@ -0,0 +1,15 @@ +mentee +mentees +mentee's +asgi +storages +s3boto3 +phonenumber +modelfields +multiselectfield +jitsi +availabilities +V2 +V1 +dateparse +breakpoint