diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy-test.yml similarity index 76% rename from .github/workflows/deploy.yml rename to .github/workflows/deploy-test.yml index 4446fa95..2c6aec4f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy-test.yml @@ -1,4 +1,4 @@ -name: deploy +name: Deploy Test Environment on: workflow_dispatch @@ -6,11 +6,11 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: wshihadeh/docker-deployment-action@v2 with: remote_docker_host: webstrom@server.strom.sk ssh_private_key: ${{ secrets.WEBSTROM_DEPLOY_SSH_PRIVATE_KEY }} ssh_public_key: ${{ secrets.WEBSTROM_DEPLOY_SSH_PUBLIC_KEY }} - stack_file_name: compose.yaml + stack_file_name: deployment/compose-test.yaml args: up --build --force-recreate --detach diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8192b463..13698a6b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,7 +16,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Upgrade pip run: pip3 install --upgrade pip @@ -37,7 +37,7 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Upgrade pip run: pip3 install --upgrade pip diff --git a/.github/workflows/migrate-test.yml b/.github/workflows/migrate-test.yml new file mode 100644 index 00000000..7c638d54 --- /dev/null +++ b/.github/workflows/migrate-test.yml @@ -0,0 +1,16 @@ +name: Migrate Test Environment + +on: workflow_dispatch + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: wshihadeh/docker-deployment-action@v2 + with: + remote_docker_host: webstrom@server.strom.sk + ssh_private_key: ${{ secrets.WEBSTROM_DEPLOY_SSH_PRIVATE_KEY }} + ssh_public_key: ${{ secrets.WEBSTROM_DEPLOY_SSH_PUBLIC_KEY }} + stack_file_name: deployment/compose-test.yaml + args: run --rm webstrom-backend python manage.py migrate --noinput diff --git a/.gitignore b/.gitignore index a6d9e34b..b2a5c20a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,9 +7,6 @@ db.sqlite3-journal /media /protected_media -**/migrations/** -!**/migrations/__init__.py - fixtures/sources !fixtures/sources/*.py !fixtures/sources/requirements.txt diff --git a/.vscode/settings.json b/.vscode/settings.json index 3784257a..05430d69 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,8 +6,11 @@ "editor.defaultFormatter": "ms-python.autopep8", "editor.formatOnSave": true, }, + "files.associations": { + "compose-*.yaml": "dockercompose" + }, "git.branchProtection": [ "master" ], "git.branchProtectionPrompt": "alwaysCommitToNewBranch" -} \ No newline at end of file +} diff --git a/Pipfile b/Pipfile index 334e3e25..2f58e7f1 100644 --- a/Pipfile +++ b/Pipfile @@ -4,14 +4,16 @@ verify_ssl = true name = "pypi" [packages] -daphne = "~=4.0.0" dj-rest-auth = "~=5.0.1" -django = "~=3.2.23" +django = "~=4.2.11" django-allauth = "~=0.58.2" django-filter = "~=23.5" +django-sendfile2 = "~=0.7.1" djangorestframework = "~=3.14.0" drf-writable-nested = "~=0.7.0" +gunicorn = "~=22.0.0" pillow = "~=10.3.0" +psycopg = "~=3.1.18" python-magic = "~=0.4.27" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 358d5aeb..bae48674 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "7df0459bbb2186ea13bb8c25ce90a415fc188092d1f00f88ca5d6350af79aa21" + "sha256": "fd2f8d27e2405d6db987ee9460aba608c0354df14453b74bec50df6804f7ef48" }, "pipfile-spec": 6, "requires": {}, @@ -22,93 +22,86 @@ "markers": "python_version >= '3.8'", "version": "==3.8.1" }, - "attrs": { - "hashes": [ - "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", - "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" - ], - "markers": "python_version >= '3.7'", - "version": "==23.2.0" - }, - "autobahn": { - "hashes": [ - "sha256:ec9421c52a2103364d1ef0468036e6019ee84f71721e86b36fe19ad6966c1181" - ], - "markers": "python_version >= '3.9'", - "version": "==23.6.2" - }, - "automat": { - "hashes": [ - "sha256:c3164f8742b9dc440f3682482d32aaff7bb53f71740dd018533f9de286b64180", - "sha256:e56beb84edad19dcc11d30e8d9b895f75deeb5ef5e96b84a467066b3b84bb04e" - ], - "version": "==22.10.0" - }, "certifi": { "hashes": [ - "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", - "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" + "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", + "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90" ], "markers": "python_version >= '3.6'", - "version": "==2024.2.2" + "version": "==2024.7.4" }, "cffi": { "hashes": [ - "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc", - "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a", - "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417", - "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab", - "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520", - "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36", - "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743", - "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8", - "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed", - "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684", - "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56", - "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324", - "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d", - "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235", - "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e", - "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088", - "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000", - "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7", - "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e", - "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673", - "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c", - "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe", - "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2", - "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098", - "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8", - "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a", - "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0", - "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b", - "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896", - "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e", - "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9", - "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2", - "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b", - "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6", - "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404", - "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f", - "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0", - "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4", - "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc", - "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936", - "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba", - "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872", - "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb", - "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614", - "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1", - "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d", - "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969", - "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b", - "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4", - "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627", - "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956", - "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357" + "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f", + "sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab", + "sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499", + "sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058", + "sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693", + "sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb", + "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377", + "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885", + "sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2", + "sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401", + "sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4", + "sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b", + "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59", + "sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f", + "sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c", + "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555", + "sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa", + "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424", + "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb", + "sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2", + "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8", + "sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e", + "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9", + "sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82", + "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828", + "sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759", + "sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc", + "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118", + "sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf", + "sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932", + "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a", + "sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29", + "sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206", + "sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2", + "sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c", + "sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c", + "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0", + "sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a", + "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195", + "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6", + "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9", + "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc", + "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb", + "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0", + "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7", + "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb", + "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a", + "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492", + "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720", + "sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42", + "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7", + "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d", + "sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d", + "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb", + "sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4", + "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2", + "sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b", + "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8", + "sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e", + "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204", + "sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3", + "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150", + "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4", + "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76", + "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e", + "sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb", + "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91" ], "markers": "platform_python_implementation != 'PyPy'", - "version": "==1.16.0" + "version": "==1.17.0" }, "charset-normalizer": { "hashes": [ @@ -206,59 +199,38 @@ "markers": "python_full_version >= '3.7.0'", "version": "==3.3.2" }, - "constantly": { - "hashes": [ - "sha256:3fd9b4d1c3dc1ec9757f3c52aef7e53ad9323dbe39f51dfd4c43853b68dfa3f9", - "sha256:aa92b70a33e2ac0bb33cd745eb61776594dc48764b06c35e0efd050b7f1c7cbd" - ], - "markers": "python_version >= '3.8'", - "version": "==23.10.4" - }, "cryptography": { "hashes": [ - "sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee", - "sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576", - "sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d", - "sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30", - "sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413", - "sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb", - "sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da", - "sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4", - "sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd", - "sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc", - "sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8", - "sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1", - "sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc", - "sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e", - "sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8", - "sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940", - "sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400", - "sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7", - "sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16", - "sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278", - "sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74", - "sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec", - "sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1", - "sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2", - "sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c", - "sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922", - "sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a", - "sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6", - "sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1", - "sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e", - "sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac", - "sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7" + "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709", + "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069", + "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2", + "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b", + "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e", + "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70", + "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778", + "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22", + "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895", + "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf", + "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431", + "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f", + "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947", + "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74", + "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc", + "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66", + "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66", + "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf", + "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f", + "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5", + "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e", + "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f", + "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55", + "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1", + "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47", + "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5", + "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0" ], "markers": "python_version >= '3.7'", - "version": "==42.0.5" - }, - "daphne": { - "hashes": [ - "sha256:a288ece46012b6b719c37150be67c69ebfca0793a8521bf821533bad983179b2", - "sha256:cce9afc8f49a4f15d4270b8cfb0e0fe811b770a5cc795474e97e4da287497666" - ], - "index": "pypi", - "version": "==4.0.0" + "version": "==43.0.0" }, "defusedxml": { "hashes": [ @@ -277,11 +249,11 @@ }, "django": { "hashes": [ - "sha256:7ca38a78654aee72378594d63e51636c04b8e28574f5505dff630895b5472777", - "sha256:a52ea7fcf280b16f7b739cec38fa6d3f8953a5456986944c3ca97e79882b4e38" + "sha256:61ee4a130efb8c451ef3467c67ca99fdce400fedd768634efc86a68c18d80d30", + "sha256:c77f926b81129493961e19c0e02188f8d07c112a1162df69bfab178ae447f94a" ], "index": "pypi", - "version": "==3.2.25" + "version": "==4.2.15" }, "django-allauth": { "hashes": [ @@ -298,6 +270,14 @@ "index": "pypi", "version": "==23.5" }, + "django-sendfile2": { + "hashes": [ + "sha256:81971df1db77f688c4834b8540930902d0d929c64cf374bd604b81f5ac52c04a", + "sha256:b5bec07f1c9b1875a60ea74beb306e9aba964bd8b54f00b4432cb77cc35bc58c" + ], + "index": "pypi", + "version": "==0.7.1" + }, "djangorestframework": { "hashes": [ "sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8", @@ -313,12 +293,13 @@ "index": "pypi", "version": "==0.7.0" }, - "hyperlink": { + "gunicorn": { "hashes": [ - "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", - "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4" + "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9", + "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63" ], - "version": "==21.0.0" + "index": "pypi", + "version": "==22.0.0" }, "idna": { "hashes": [ @@ -328,13 +309,6 @@ "markers": "python_version >= '3.5'", "version": "==3.7" }, - "incremental": { - "hashes": [ - "sha256:912feeb5e0f7e0188e6f42241d2f450002e11bbc0937c65865045854c24c0bd0", - "sha256:b864a1f30885ee72c5ac2835a761b8fe8aa9c28b9395cacf27286602688d3e51" - ], - "version": "==22.10.0" - }, "oauthlib": { "hashes": [ "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", @@ -343,6 +317,14 @@ "markers": "python_version >= '3.6'", "version": "==3.2.2" }, + "packaging": { + "hashes": [ + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + ], + "markers": "python_version >= '3.8'", + "version": "==24.1" + }, "pillow": { "hashes": [ "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c", @@ -418,21 +400,13 @@ "index": "pypi", "version": "==10.3.0" }, - "pyasn1": { + "psycopg": { "hashes": [ - "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c", - "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473" + "sha256:32f5862ab79f238496236f97fe374a7ab55b4b4bb839a74802026544735f9a07", + "sha256:898a29f49ac9c903d554f5a6cdc44a8fc564325557c18f82e51f39c1f4fc2aeb" ], - "markers": "python_version >= '3.8'", - "version": "==0.6.0" - }, - "pyasn1-modules": { - "hashes": [ - "sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6", - "sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b" - ], - "markers": "python_version >= '3.8'", - "version": "==0.4.0" + "index": "pypi", + "version": "==3.1.20" }, "pycparser": { "hashes": [ @@ -447,18 +421,11 @@ "crypto" ], "hashes": [ - "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de", - "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320" - ], - "markers": "python_version >= '3.7'", - "version": "==2.8.0" - }, - "pyopenssl": { - "hashes": [ - "sha256:17ed5be5936449c5418d1cd269a1a9e9081bc54c17aed272b45856a3d3dc86ad", - "sha256:cabed4bfaa5df9f1a16c0ef64a0cb65318b5cd077a7eda7d6970131ca2f41a6f" + "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850", + "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c" ], - "version": "==24.1.0" + "markers": "python_version >= '3.8'", + "version": "==2.9.0" }, "python-magic": { "hashes": [ @@ -484,11 +451,11 @@ }, "requests": { "hashes": [ - "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", - "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" + "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", + "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" ], - "markers": "python_version >= '3.7'", - "version": "==2.31.0" + "markers": "python_version >= '3.8'", + "version": "==2.32.3" }, "requests-oauthlib": { "hashes": [ @@ -498,113 +465,29 @@ "markers": "python_version >= '3.4'", "version": "==2.0.0" }, - "service-identity": { - "hashes": [ - "sha256:6829c9d62fb832c2e1c435629b0a8c476e1929881f28bee4d20bc24161009221", - "sha256:a28caf8130c8a5c1c7a6f5293faaf239bbfb7751e4862436920ee6f2616f568a" - ], - "version": "==24.1.0" - }, - "setuptools": { - "hashes": [ - "sha256:659e902e587e77fab8212358f5b03977b5f0d18d4724310d4a093929fee4ca1a", - "sha256:b6df12d754b505e4ca283c61582d5578db83ae2f56a979b3bc9a8754705ae3bf" - ], - "markers": "python_version >= '3.8'", - "version": "==69.4.0" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, "sqlparse": { "hashes": [ - "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3", - "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c" - ], - "markers": "python_version >= '3.5'", - "version": "==0.4.4" - }, - "twisted": { - "extras": [ - "tls" - ], - "hashes": [ - "sha256:039f2e6a49ab5108abd94de187fa92377abe5985c7a72d68d0ad266ba19eae63", - "sha256:6b38b6ece7296b5e122c9eb17da2eeab3d98a198f50ca9efd00fb03e5b4fd4ae" - ], - "markers": "python_full_version >= '3.8.0'", - "version": "==24.3.0" - }, - "txaio": { - "hashes": [ - "sha256:aaea42f8aad50e0ecfb976130ada140797e9dcb85fad2cf72b0f37f8cefcb490", - "sha256:f9a9216e976e5e3246dfd112ad7ad55ca915606b60b84a757ac769bd404ff704" + "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4", + "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e" ], - "markers": "python_version >= '3.7'", - "version": "==23.1.1" + "markers": "python_version >= '3.8'", + "version": "==0.5.1" }, "typing-extensions": { "hashes": [ - "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", - "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], "markers": "python_version >= '3.8'", - "version": "==4.11.0" + "version": "==4.12.2" }, "urllib3": { "hashes": [ - "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", - "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" + "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", + "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" ], "markers": "python_version >= '3.8'", - "version": "==2.2.1" - }, - "zope-interface": { - "hashes": [ - "sha256:014bb94fe6bf1786da1aa044eadf65bc6437bcb81c451592987e5be91e70a91e", - "sha256:01a0b3dd012f584afcf03ed814bce0fc40ed10e47396578621509ac031be98bf", - "sha256:10cde8dc6b2fd6a1d0b5ca4be820063e46ddba417ab82bcf55afe2227337b130", - "sha256:187f7900b63845dcdef1be320a523dbbdba94d89cae570edc2781eb55f8c2f86", - "sha256:1b0c4c90e5eefca2c3e045d9f9ed9f1e2cdbe70eb906bff6b247e17119ad89a1", - "sha256:22e8a218e8e2d87d4d9342aa973b7915297a08efbebea5b25900c73e78ed468e", - "sha256:26c9a37fb395a703e39b11b00b9e921c48f82b6e32cc5851ad5d0618cd8876b5", - "sha256:2bb78c12c1ad3a20c0d981a043d133299117b6854f2e14893b156979ed4e1d2c", - "sha256:2c3cfb272bcb83650e6695d49ae0d14dd06dc694789a3d929f23758557a23d92", - "sha256:2f32010ffb87759c6a3ad1c65ed4d2e38e51f6b430a1ca11cee901ec2b42e021", - "sha256:3c8731596198198746f7ce2a4487a0edcbc9ea5e5918f0ab23c4859bce56055c", - "sha256:40aa8c8e964d47d713b226c5baf5f13cdf3a3169c7a2653163b17ff2e2334d10", - "sha256:4137025731e824eee8d263b20682b28a0bdc0508de9c11d6c6be54163e5b7c83", - "sha256:46034be614d1f75f06e7dcfefba21d609b16b38c21fc912b01a99cb29e58febb", - "sha256:483e118b1e075f1819b3c6ace082b9d7d3a6a5eb14b2b375f1b80a0868117920", - "sha256:4d6b229f5e1a6375f206455cc0a63a8e502ed190fe7eb15e94a312dc69d40299", - "sha256:567d54c06306f9c5b6826190628d66753b9f2b0422f4c02d7c6d2b97ebf0a24e", - "sha256:5683aa8f2639016fd2b421df44301f10820e28a9b96382a6e438e5c6427253af", - "sha256:600101f43a7582d5b9504a7c629a1185a849ce65e60fca0f6968dfc4b76b6d39", - "sha256:62e32f02b3f26204d9c02c3539c802afc3eefb19d601a0987836ed126efb1f21", - "sha256:69dedb790530c7ca5345899a1b4cb837cc53ba669051ea51e8c18f82f9389061", - "sha256:72d5efecad16c619a97744a4f0b67ce1bcc88115aa82fcf1dc5be9bb403bcc0b", - "sha256:8d407e0fd8015f6d5dfad481309638e1968d70e6644e0753f229154667dd6cd5", - "sha256:a058e6cf8d68a5a19cb5449f42a404f0d6c2778b897e6ce8fadda9cea308b1b0", - "sha256:a1adc14a2a9d5e95f76df625a9b39f4709267a483962a572e3f3001ef90ea6e6", - "sha256:a56fe1261230093bfeedc1c1a6cd6f3ec568f9b07f031c9a09f46b201f793a85", - "sha256:ad4524289d8dbd6fb5aa17aedb18f5643e7d48358f42c007a5ee51a2afc2a7c5", - "sha256:afa0491a9f154cf8519a02026dc85a416192f4cb1efbbf32db4a173ba28b289a", - "sha256:bf34840e102d1d0b2d39b1465918d90b312b1119552cebb61a242c42079817b9", - "sha256:c40df4aea777be321b7e68facb901bc67317e94b65d9ab20fb96e0eb3c0b60a1", - "sha256:d0e7321557c702bd92dac3c66a2f22b963155fdb4600133b6b29597f62b71b12", - "sha256:d165d7774d558ea971cb867739fb334faf68fc4756a784e689e11efa3becd59e", - "sha256:e78a183a3c2f555c2ad6aaa1ab572d1c435ba42f1dc3a7e8c82982306a19b785", - "sha256:e8fa0fb05083a1a4216b4b881fdefa71c5d9a106e9b094cd4399af6b52873e91", - "sha256:f83d6b4b22262d9a826c3bd4b2fbfafe1d0000f085ef8e44cd1328eea274ae6a", - "sha256:f95bebd0afe86b2adc074df29edb6848fc4d474ff24075e2c263d698774e108d" - ], - "markers": "python_version >= '3.7'", - "version": "==6.3" + "version": "==2.2.2" } }, "develop": { @@ -618,27 +501,27 @@ }, "astroid": { "hashes": [ - "sha256:951798f922990137ac090c53af473db7ab4e70c770e6d7fae0cec59f74411819", - "sha256:ac248253bfa4bd924a0de213707e7ebeeb3138abeb48d798784ead1e56d419d4" + "sha256:0e14202810b30da1b735827f78f5157be2bbd4a7a59b7707ca0bfc2fb4c0063a", + "sha256:413658a61eeca6202a59231abb473f932038fbcbf1666587f66d482083413a25" ], "markers": "python_full_version >= '3.8.0'", - "version": "==3.1.0" + "version": "==3.2.4" }, "autopep8": { "hashes": [ - "sha256:1fa8964e4618929488f4ec36795c7ff12924a68b8bf01366c094fc52f770b6e7", - "sha256:2bb76888c5edbcafe6aabab3c47ba534f5a2c2d245c2eddced4a30c4b4946357" + "sha256:8d6c87eba648fdcfc83e29b788910b8643171c395d9c4bcf115ece035b9c9dda", + "sha256:a203fe0fcad7939987422140ab17a930f684763bf7335bdb6709991dd7ef6c2d" ], "index": "pypi", - "version": "==2.1.0" + "version": "==2.3.1" }, "certifi": { "hashes": [ - "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", - "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" + "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", + "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90" ], "markers": "python_version >= '3.6'", - "version": "==2024.2.2" + "version": "==2024.7.4" }, "charset-normalizer": { "hashes": [ @@ -760,11 +643,11 @@ }, "django": { "hashes": [ - "sha256:7ca38a78654aee72378594d63e51636c04b8e28574f5505dff630895b5472777", - "sha256:a52ea7fcf280b16f7b739cec38fa6d3f8953a5456986944c3ca97e79882b4e38" + "sha256:61ee4a130efb8c451ef3467c67ca99fdce400fedd768634efc86a68c18d80d30", + "sha256:c77f926b81129493961e19c0e02188f8d07c112a1162df69bfab178ae447f94a" ], "index": "pypi", - "version": "==3.2.25" + "version": "==4.2.15" }, "django-rest-swagger": { "hashes": [ @@ -776,11 +659,11 @@ }, "django-typomatic": { "hashes": [ - "sha256:1903d983f73fb7ac2f47f4f44e10d1500f8f42df400cae31f1e250397d91d22e", - "sha256:2e79dbd37b562c31c541a7dd50d2c09dbf44b778ea99555d7c229d3c66f86573" + "sha256:8d6a8b8d82e5782756a16b93b8c6004af4a6743b8cb38d520c3fd1588f0f5b82", + "sha256:9b3a28fd5eb60f48aae0530e5db18d40d6a8c4b869dedb05f214de575a20d11c" ], "index": "pypi", - "version": "==2.5.0" + "version": "==2.5.2" }, "djangorestframework": { "hashes": [ @@ -815,11 +698,11 @@ }, "jinja2": { "hashes": [ - "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa", - "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90" + "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", + "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" ], "markers": "python_version >= '3.7'", - "version": "==3.1.3" + "version": "==3.1.4" }, "markupsafe": { "hashes": [ @@ -903,27 +786,27 @@ }, "platformdirs": { "hashes": [ - "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", - "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768" + "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee", + "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3" ], "markers": "python_version >= '3.8'", - "version": "==4.2.0" + "version": "==4.2.2" }, "pycodestyle": { "hashes": [ - "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f", - "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67" + "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3", + "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521" ], "markers": "python_version >= '3.8'", - "version": "==2.11.1" + "version": "==2.12.1" }, "pylint": { "hashes": [ - "sha256:507a5b60953874766d8a366e8e8c7af63e058b26345cfcb5f91f89d987fd6b74", - "sha256:6a69beb4a6f63debebaab0a3477ecd0f559aa726af4954fc948c51f7a2549e23" + "sha256:03c8e3baa1d9fb995b12c1dbe00aa6c4bcef210c2a2634374aedeb22fb4a8f8f", + "sha256:a5d01678349454806cff6d886fb072294f56a58c4761278c97fb557d708e1eb3" ], "index": "pypi", - "version": "==3.1.0" + "version": "==3.2.6" }, "pylint-django": { "hashes": [ @@ -950,131 +833,143 @@ }, "requests": { "hashes": [ - "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", - "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" + "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", + "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" ], - "markers": "python_version >= '3.7'", - "version": "==2.31.0" + "markers": "python_version >= '3.8'", + "version": "==2.32.3" }, "simplejson": { "hashes": [ - "sha256:0405984f3ec1d3f8777c4adc33eac7ab7a3e629f3b1c05fdded63acc7cf01137", - "sha256:0436a70d8eb42bea4fe1a1c32d371d9bb3b62c637969cb33970ad624d5a3336a", - "sha256:061e81ea2d62671fa9dea2c2bfbc1eec2617ae7651e366c7b4a2baf0a8c72cae", - "sha256:064300a4ea17d1cd9ea1706aa0590dcb3be81112aac30233823ee494f02cb78a", - "sha256:08889f2f597ae965284d7b52a5c3928653a9406d88c93e3161180f0abc2433ba", - "sha256:0a48679310e1dd5c9f03481799311a65d343748fe86850b7fb41df4e2c00c087", - "sha256:0b0a3eb6dd39cce23801a50c01a0976971498da49bc8a0590ce311492b82c44b", - "sha256:0d2d5119b1d7a1ed286b8af37357116072fc96700bce3bec5bb81b2e7057ab41", - "sha256:0d551dc931638e2102b8549836a1632e6e7cf620af3d093a7456aa642bff601d", - "sha256:1018bd0d70ce85f165185d2227c71e3b1e446186f9fa9f971b69eee223e1e3cd", - "sha256:11c39fbc4280d7420684494373b7c5904fa72a2b48ef543a56c2d412999c9e5d", - "sha256:11cc3afd8160d44582543838b7e4f9aa5e97865322844b75d51bf4e0e413bb3e", - "sha256:1537b3dd62d8aae644f3518c407aa8469e3fd0f179cdf86c5992792713ed717a", - "sha256:16ca9c90da4b1f50f089e14485db8c20cbfff2d55424062791a7392b5a9b3ff9", - "sha256:176a1b524a3bd3314ed47029a86d02d5a95cc0bee15bd3063a1e1ec62b947de6", - "sha256:18955c1da6fc39d957adfa346f75226246b6569e096ac9e40f67d102278c3bcb", - "sha256:1bb5b50dc6dd671eb46a605a3e2eb98deb4a9af787a08fcdddabe5d824bb9664", - "sha256:1c768e7584c45094dca4b334af361e43b0aaa4844c04945ac7d43379eeda9bc2", - "sha256:1dd4f692304854352c3e396e9b5f0a9c9e666868dd0bdc784e2ac4c93092d87b", - "sha256:25785d038281cd106c0d91a68b9930049b6464288cea59ba95b35ee37c2d23a5", - "sha256:287e39ba24e141b046812c880f4619d0ca9e617235d74abc27267194fc0c7835", - "sha256:2c1467d939932901a97ba4f979e8f2642415fcf02ea12f53a4e3206c9c03bc17", - "sha256:2c433a412e96afb9a3ce36fa96c8e61a757af53e9c9192c97392f72871e18e69", - "sha256:2d022b14d7758bfb98405672953fe5c202ea8a9ccf9f6713c5bd0718eba286fd", - "sha256:2f98d918f7f3aaf4b91f2b08c0c92b1774aea113334f7cde4fe40e777114dbe6", - "sha256:2fc697be37585eded0c8581c4788fcfac0e3f84ca635b73a5bf360e28c8ea1a2", - "sha256:3194cd0d2c959062b94094c0a9f8780ffd38417a5322450a0db0ca1a23e7fbd2", - "sha256:332c848f02d71a649272b3f1feccacb7e4f7e6de4a2e6dc70a32645326f3d428", - "sha256:346820ae96aa90c7d52653539a57766f10f33dd4be609206c001432b59ddf89f", - "sha256:3471e95110dcaf901db16063b2e40fb394f8a9e99b3fe9ee3acc6f6ef72183a2", - "sha256:3848427b65e31bea2c11f521b6fc7a3145d6e501a1038529da2391aff5970f2f", - "sha256:39b6d79f5cbfa3eb63a869639cfacf7c41d753c64f7801efc72692c1b2637ac7", - "sha256:3e74355cb47e0cd399ead3477e29e2f50e1540952c22fb3504dda0184fc9819f", - "sha256:3f39bb1f6e620f3e158c8b2eaf1b3e3e54408baca96a02fe891794705e788637", - "sha256:40847f617287a38623507d08cbcb75d51cf9d4f9551dd6321df40215128325a3", - "sha256:4280e460e51f86ad76dc456acdbfa9513bdf329556ffc8c49e0200878ca57816", - "sha256:445a96543948c011a3a47c8e0f9d61e9785df2544ea5be5ab3bc2be4bd8a2565", - "sha256:4969d974d9db826a2c07671273e6b27bc48e940738d768fa8f33b577f0978378", - "sha256:49aaf4546f6023c44d7e7136be84a03a4237f0b2b5fb2b17c3e3770a758fc1a0", - "sha256:49e0e3faf3070abdf71a5c80a97c1afc059b4f45a5aa62de0c2ca0444b51669b", - "sha256:49f9da0d6cd17b600a178439d7d2d57c5ef01f816b1e0e875e8e8b3b42db2693", - "sha256:4a8c3cc4f9dfc33220246760358c8265dad6e1104f25f0077bbca692d616d358", - "sha256:4d36081c0b1c12ea0ed62c202046dca11438bee48dd5240b7c8de8da62c620e9", - "sha256:4edcd0bf70087b244ba77038db23cd98a1ace2f91b4a3ecef22036314d77ac23", - "sha256:554313db34d63eac3b3f42986aa9efddd1a481169c12b7be1e7512edebff8eaf", - "sha256:5675e9d8eeef0aa06093c1ff898413ade042d73dc920a03e8cea2fb68f62445a", - "sha256:60848ab779195b72382841fc3fa4f71698a98d9589b0a081a9399904487b5832", - "sha256:66e5dc13bfb17cd6ee764fc96ccafd6e405daa846a42baab81f4c60e15650414", - "sha256:6779105d2fcb7fcf794a6a2a233787f6bbd4731227333a072d8513b252ed374f", - "sha256:6ad331349b0b9ca6da86064a3599c425c7a21cd41616e175ddba0866da32df48", - "sha256:6f0a0b41dd05eefab547576bed0cf066595f3b20b083956b1405a6f17d1be6ad", - "sha256:73a8a4653f2e809049999d63530180d7b5a344b23a793502413ad1ecea9a0290", - "sha256:778331444917108fa8441f59af45886270d33ce8a23bfc4f9b192c0b2ecef1b3", - "sha256:7cb98be113911cb0ad09e5523d0e2a926c09a465c9abb0784c9269efe4f95917", - "sha256:7d74beca677623481810c7052926365d5f07393c72cbf62d6cce29991b676402", - "sha256:7f2398361508c560d0bf1773af19e9fe644e218f2a814a02210ac2c97ad70db0", - "sha256:8434dcdd347459f9fd9c526117c01fe7ca7b016b6008dddc3c13471098f4f0dc", - "sha256:8a390e56a7963e3946ff2049ee1eb218380e87c8a0e7608f7f8790ba19390867", - "sha256:92c4a4a2b1f4846cd4364855cbac83efc48ff5a7d7c06ba014c792dd96483f6f", - "sha256:9300aee2a8b5992d0f4293d88deb59c218989833e3396c824b69ba330d04a589", - "sha256:9453419ea2ab9b21d925d0fd7e3a132a178a191881fab4169b6f96e118cc25bb", - "sha256:9652e59c022e62a5b58a6f9948b104e5bb96d3b06940c6482588176f40f4914b", - "sha256:972a7833d4a1fcf7a711c939e315721a88b988553fc770a5b6a5a64bd6ebeba3", - "sha256:9c1a4393242e321e344213a90a1e3bf35d2f624aa8b8f6174d43e3c6b0e8f6eb", - "sha256:9e038c615b3906df4c3be8db16b3e24821d26c55177638ea47b3f8f73615111c", - "sha256:9e4c166f743bb42c5fcc60760fb1c3623e8fda94f6619534217b083e08644b46", - "sha256:9eb117db8d7ed733a7317c4215c35993b815bf6aeab67523f1f11e108c040672", - "sha256:9eb442a2442ce417801c912df68e1f6ccfcd41577ae7274953ab3ad24ef7d82c", - "sha256:a3cd18e03b0ee54ea4319cdcce48357719ea487b53f92a469ba8ca8e39df285e", - "sha256:a8617625369d2d03766413bff9e64310feafc9fc4f0ad2b902136f1a5cd8c6b0", - "sha256:a970a2e6d5281d56cacf3dc82081c95c1f4da5a559e52469287457811db6a79b", - "sha256:aad7405c033d32c751d98d3a65801e2797ae77fac284a539f6c3a3e13005edc4", - "sha256:adcb3332979cbc941b8fff07181f06d2b608625edc0a4d8bc3ffc0be414ad0c4", - "sha256:af9c7e6669c4d0ad7362f79cb2ab6784d71147503e62b57e3d95c4a0f222c01c", - "sha256:b01fda3e95d07a6148702a641e5e293b6da7863f8bc9b967f62db9461330562c", - "sha256:b8d940fd28eb34a7084877747a60873956893e377f15a32ad445fe66c972c3b8", - "sha256:bccb3e88ec26ffa90f72229f983d3a5d1155e41a1171190fa723d4135523585b", - "sha256:bcedf4cae0d47839fee7de344f96b5694ca53c786f28b5f773d4f0b265a159eb", - "sha256:be893258d5b68dd3a8cba8deb35dc6411db844a9d35268a8d3793b9d9a256f80", - "sha256:c0521e0f07cb56415fdb3aae0bbd8701eb31a9dfef47bb57206075a0584ab2a2", - "sha256:c594642d6b13d225e10df5c16ee15b3398e21a35ecd6aee824f107a625690374", - "sha256:c87c22bd6a987aca976e3d3e23806d17f65426191db36d40da4ae16a6a494cbc", - "sha256:c9ac1c2678abf9270e7228133e5b77c6c3c930ad33a3c1dfbdd76ff2c33b7b50", - "sha256:d0e5ffc763678d48ecc8da836f2ae2dd1b6eb2d27a48671066f91694e575173c", - "sha256:d0f402e787e6e7ee7876c8b05e2fe6464820d9f35ba3f172e95b5f8b699f6c7f", - "sha256:d222a9ed082cd9f38b58923775152003765016342a12f08f8c123bf893461f28", - "sha256:d94245caa3c61f760c4ce4953cfa76e7739b6f2cbfc94cc46fff6c050c2390c5", - "sha256:de9a2792612ec6def556d1dc621fd6b2073aff015d64fba9f3e53349ad292734", - "sha256:e2f5a398b5e77bb01b23d92872255e1bcb3c0c719a3be40b8df146570fe7781a", - "sha256:e8dd53a8706b15bc0e34f00e6150fbefb35d2fd9235d095b4f83b3c5ed4fa11d", - "sha256:e9eb3cff1b7d71aa50c89a0536f469cb8d6dcdd585d8f14fb8500d822f3bdee4", - "sha256:ed628c1431100b0b65387419551e822987396bee3c088a15d68446d92f554e0c", - "sha256:ef7938a78447174e2616be223f496ddccdbf7854f7bf2ce716dbccd958cc7d13", - "sha256:f1c70249b15e4ce1a7d5340c97670a95f305ca79f376887759b43bb33288c973", - "sha256:f3c7363a8cb8c5238878ec96c5eb0fc5ca2cb11fc0c7d2379863d342c6ee367a", - "sha256:fbbcc6b0639aa09b9649f36f1bcb347b19403fe44109948392fbb5ea69e48c3e", - "sha256:febffa5b1eda6622d44b245b0685aff6fb555ce0ed734e2d7b1c3acd018a2cff", - "sha256:ff836cd4041e16003549449cc0a5e372f6b6f871eb89007ab0ee18fb2800fded" + "sha256:01c6657485393f2e9b8177c77a7634f13ebe70d5e6de150aae1677d91516ce6b", + "sha256:0552eb06e7234da892e1d02365cd2b7b2b1f8233aa5aabdb2981587b7cc92ea0", + "sha256:06662392e4913dc8846d6a71a6d5de86db5fba244831abe1dd741d62a4136764", + "sha256:0733ecd95ae03ae718ec74aad818f5af5f3155d596f7b242acbc1621e765e5fb", + "sha256:0766ca6222b410e08e0053a0dda3606cafb3973d5d00538307f631bb59743396", + "sha256:0791f64fed7d4abad639491f8a6b1ba56d3c604eb94b50f8697359b92d983f36", + "sha256:08f9b443a94e72dd02c87098c96886d35790e79e46b24e67accafbf13b73d43b", + "sha256:0959e6cb62e3994b5a40e31047ff97ef5c4138875fae31659bead691bed55896", + "sha256:0a32859d45d7b85fb803bb68f6bee14526991a1190269116c33399fa0daf9bbf", + "sha256:0b5ddd2c7d1d3f4d23224bc8a04bbf1430ae9a8149c05b90f8fc610f7f857a23", + "sha256:0bc5544e3128891bf613b9f71813ee2ec9c11574806f74dd8bb84e5e95bf64a2", + "sha256:101a3c8392028cd704a93c7cba8926594e775ca3c91e0bee82144e34190903f1", + "sha256:1069143a8fb3905e1bc0696c62be7e3adf812e9f1976ac9ae15b05112ff57cc9", + "sha256:1773cabfba66a6337b547e45dafbd471b09487370bcab75bd28f626520410d29", + "sha256:1a53a07320c5ff574d8b1a89c937ce33608832f166f39dff0581ac43dc979abd", + "sha256:1bd41f2cb1a2c57656ceff67b12d005cb255c728265e222027ad73193a04005a", + "sha256:1c49eeb94b8f09dc8a5843c156a22b8bde6aa1ddc65ca8ddc62dddcc001e6a2d", + "sha256:1df0aaf1cb787fdf34484ed4a1f0c545efd8811f6028623290fef1a53694e597", + "sha256:1e557712fc79f251673aeb3fad3501d7d4da3a27eff0857af2e1d1afbbcf6685", + "sha256:1e662336db50ad665777e6548b5076329a94a0c3d4a0472971c588b3ef27de3a", + "sha256:212fce86a22188b0c7f53533b0f693ea9605c1a0f02c84c475a30616f55a744d", + "sha256:23228037dc5d41c36666384062904d74409a62f52283d9858fa12f4c22cffad1", + "sha256:23833ee7e791ec968b744dfee2a2d39df7152050051096caf4296506d75608d8", + "sha256:256e09d0f94d9c3d177d9e95fd27a68c875a4baa2046633df387b86b652f5747", + "sha256:2876027ebdd599d730d36464debe84619b0368e9a642ca6e7c601be55aed439e", + "sha256:2a6a750d3c7461b1c47cfc6bba8d9e57a455e7c5f80057d2a82f738040dd1129", + "sha256:2a954b30810988feeabde843e3263bf187697e0eb5037396276db3612434049b", + "sha256:2b737a5fefedb8333fa50b8db3dcc9b1d18fd6c598f89fa7debff8b46bf4e511", + "sha256:2c78293470313aefa9cfc5e3f75ca0635721fb016fb1121c1c5b0cb8cc74712a", + "sha256:2f56eb03bc9e432bb81adc8ecff2486d39feb371abb442964ffb44f6db23b332", + "sha256:32a3ada8f3ea41db35e6d37b86dade03760f804628ec22e4fe775b703d567426", + "sha256:37105d1d708365b91165e1a6e505bdecc88637091348cf4b6adcdcb4f5a5fb8b", + "sha256:3bbcdc438dc1683b35f7a8dc100960c721f922f9ede8127f63bed7dfded4c64c", + "sha256:3dc5c1a85ff388e98ea877042daec3d157b6db0d85bac6ba5498034689793e7e", + "sha256:42e5acf80d4d971238d4df97811286a044d720693092b20a56d5e56b7dcc5d09", + "sha256:49549e3d81ab4a58424405aa545602674d8c35c20e986b42bb8668e782a94bac", + "sha256:49cc4c7b940d43bd12bf87ec63f28cbc4964fc4e12c031cc8cd01650f43eb94e", + "sha256:4a0710d1a5e41c4f829caa1572793dd3130c8d65c2b194c24ff29c4c305c26e0", + "sha256:4dfa420bb9225dd33b6efdabde7c6a671b51150b9b1d9c4e5cd74d3b420b3fe1", + "sha256:50d8b742d74c449c4dcac570d08ce0f21f6a149d2d9cf7652dbf2ba9a1bc729a", + "sha256:56134bbafe458a7b21f6fddbf889d36bec6d903718f4430768e3af822f8e27c2", + "sha256:5bf6a3b9a7d7191471b464fe38f684df10eb491ec9ea454003edb45a011ab187", + "sha256:5d9e8f836688a8fabe6a6b41b334aa550a6823f7b4ac3d3712fc0ad8655be9a8", + "sha256:619756f1dd634b5bdf57d9a3914300526c3b348188a765e45b8b08eabef0c94e", + "sha256:6300680d83a399be2b8f3b0ef7ef90b35d2a29fe6e9c21438097e0938bbc1564", + "sha256:637c4d4b81825c1f4d651e56210bd35b5604034b192b02d2d8f17f7ce8c18f42", + "sha256:66a0399e21c2112acacfebf3d832ebe2884f823b1c7e6d1363f2944f1db31a99", + "sha256:67a20641afebf4cfbcff50061f07daad1eace6e7b31d7622b6fa2c40d43900ba", + "sha256:6890ff9cf0bd2e1d487e2a8869ebd620a44684c0a9667fa5ee751d099d5d84c8", + "sha256:6d43e24b88c80f997081503f693be832fc90854f278df277dd54f8a4c847ab61", + "sha256:6ef9383c5e05f445be60f1735c1816163c874c0b1ede8bb4390aff2ced34f333", + "sha256:6f455672f4738b0f47183c5896e3606cd65c9ddee3805a4d18e8c96aa3f47c84", + "sha256:6fea0716c593dabb4392c4996d4e902a83b2428e6da82938cf28a523a11eb277", + "sha256:7017329ca8d4dca94ad5e59f496e5fc77630aecfc39df381ffc1d37fb6b25832", + "sha256:7137e69c6781ecf23afab064be94a277236c9cba31aa48ff1a0ec3995c69171e", + "sha256:72e8abbc86fcac83629a030888b45fed3a404d54161118be52cb491cd6975d3e", + "sha256:7355c7203353c36d46c4e7b6055293b3d2be097bbc5e2874a2b8a7259f0325dd", + "sha256:76f8c28fe2d426182405b18ddf3001fce47835a557dc15c3d8bdea01c03361da", + "sha256:7923878b7a0142d39763ec2dbecff3053c1bedd3653585a8474666e420fe83f5", + "sha256:7a7bfad839c624e139a4863007233a3f194e7c51551081f9789cba52e4da5167", + "sha256:7b5c472099b39b274dcde27f1113db8d818c9aa3ba8f78cbb8ad04a4c1ac2118", + "sha256:7c0104b4b7d2c75ccedbf1d9d5a3bd2daa75e51053935a44ba012e2fd4c43752", + "sha256:7e062767ac165df9a46963f5735aa4eee0089ec1e48b3f2ec46182754b96f55e", + "sha256:7e2a098c21ad8924076a12b6c178965d88a0ad75d1de67e1afa0a66878f277a5", + "sha256:817abad79241ed4a507b3caf4d3f2be5079f39d35d4c550a061988986bffd2ec", + "sha256:83c87706265ae3028e8460d08b05f30254c569772e859e5ba61fe8af2c883468", + "sha256:89b35433186e977fa86ff1fd179c1fadff39cfa3afa1648dab0b6ca53153acd9", + "sha256:8e086896c36210ab6050f2f9f095a5f1e03c83fa0e7f296d6cba425411364680", + "sha256:8f41bb5370b34f63171e65fdb00e12be1d83675cecb23e627df26f4c88dfc021", + "sha256:934a50a614fb831614db5dbfba35127ee277624dda4d15895c957d2f5d48610c", + "sha256:93be280fc69a952c76e261036312c20b910e7fa9e234f1d89bdfe3fa34f8a023", + "sha256:951095be8d4451a7182403354c22ec2de3e513e0cc40408b689af08d02611588", + "sha256:a0782cb9bf827f0c488b6aa0f2819f618308a3caf2973cfd792e45d631bec4db", + "sha256:ab69f811a660c362651ae395eba8ce84f84c944cea0df5718ea0ba9d1e4e7252", + "sha256:ad0e0b1ce9bd3edb5cf64b5b5b76eacbfdac8c5367153aeeec8a8b1407f68342", + "sha256:add8850db04b98507a8b62d248a326ecc8561e6d24336d1ca5c605bbfaab4cad", + "sha256:afab2f7f2486a866ff04d6d905e9386ca6a231379181a3838abce1f32fbdcc37", + "sha256:b5587feda2b65a79da985ae6d116daf6428bf7489992badc29fc96d16cd27b05", + "sha256:b9198c1f1f8910a3b86b60f4fe2556d9d28d3fefe35bffe6be509a27402e694d", + "sha256:bc164f32dd9691e7082ce5df24b4cf8c6c394bbf9bdeeb5d843127cd07ab8ad2", + "sha256:bcde83a553a96dc7533736c547bddaa35414a2566ab0ecf7d3964fc4bdb84c11", + "sha256:c40df31a75de98db2cdfead6074d4449cd009e79f54c1ebe5e5f1f153c68ad20", + "sha256:c4f614581b61a26fbbba232a1391f6cee82bc26f2abbb6a0b44a9bba25c56a1c", + "sha256:c9bedebdc5fdad48af8783022bae307746d54006b783007d1d3c38e10872a2c6", + "sha256:cb324bb903330cbb35d87cce367a12631cd5720afa06e5b9c906483970946da6", + "sha256:d00313681015ac498e1736b304446ee6d1c72c5b287cd196996dad84369998f7", + "sha256:d0b0efc7279d768db7c74d3d07f0b5c81280d16ae3fb14e9081dc903e8360771", + "sha256:d0d5a63f1768fed7e78cf55712dee81f5a345e34d34224f3507ebf71df2b754d", + "sha256:d1b8b4d6379fe55f471914345fe6171d81a18649dacf3248abfc9c349b4442eb", + "sha256:d36608557b4dcd7a62c29ad4cd7c5a1720bbf7dc942eff9dc42d2c542a5f042d", + "sha256:d43c2d7504eda566c50203cdc9dc043aff6f55f1b7dae0dcd79dfefef9159d1c", + "sha256:d73efb03c5b39249c82488a994f0998f9e4399e3d085209d2120503305ba77a8", + "sha256:d936ae682d5b878af9d9eb4d8bb1fdd5e41275c8eb59ceddb0aeed857bb264a2", + "sha256:dd011fc3c1d88b779645495fdb8189fb318a26981eebcce14109460e062f209b", + "sha256:dd5b9b1783e14803e362a558680d88939e830db2466f3fa22df5c9319f8eea94", + "sha256:dd6a7dabcc4c32daf601bc45e01b79175dde4b52548becea4f9545b0a4428169", + "sha256:dd7230d061e755d60a4d5445bae854afe33444cdb182f3815cff26ac9fb29a15", + "sha256:e0d2b00ecbcd1a3c5ea1abc8bb99a26508f758c1759fd01c3be482a3655a176f", + "sha256:e1a1452ad5723ff129b081e3c8aa4ba56b8734fee4223355ed7b815a7ece69bc", + "sha256:e88abff510dcff903a18d11c2a75f9964e768d99c8d147839913886144b2065e", + "sha256:ea7a4a998c87c5674a27089e022110a1a08a7753f21af3baf09efe9915c23c3c", + "sha256:eb47ee773ce67476a960e2db4a0a906680c54f662521550828c0cc57d0099426", + "sha256:eed8cd98a7b24861da9d3d937f5fbfb6657350c547528a117297fe49e3960667", + "sha256:ef28c3b328d29b5e2756903aed888960bc5df39b4c2eab157ae212f70ed5bf74", + "sha256:ef59a53be400c1fad2c914b8d74c9d42384fed5174f9321dd021b7017fd40270", + "sha256:f39caec26007a2d0efab6b8b1d74873ede9351962707afab622cc2285dd26ed0", + "sha256:f8efb03ca77bd7725dfacc9254df00d73e6f43013cf39bd37ef1a8ed0ebb5165", + "sha256:fa97278ae6614346b5ca41a45a911f37a3261b57dbe4a00602048652c862c28b", + "sha256:fc3dc9fb413fc34c396f52f4c87de18d0bd5023804afa8ab5cc224deeb6a9900", + "sha256:ff7bc1bbdaa3e487c9469128bf39408e91f5573901cb852e03af378d3582c52d" ], "markers": "python_version >= '2.5' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==3.19.2" + "version": "==3.19.3" }, "sqlparse": { "hashes": [ - "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3", - "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c" + "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4", + "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e" ], - "markers": "python_version >= '3.5'", - "version": "==0.4.4" + "markers": "python_version >= '3.8'", + "version": "==0.5.1" }, "tomlkit": { "hashes": [ - "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b", - "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3" + "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", + "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79" ], - "markers": "python_version >= '3.7'", - "version": "==0.12.4" + "markers": "python_version >= '3.8'", + "version": "==0.13.2" }, "unidecode": { "hashes": [ @@ -1094,11 +989,11 @@ }, "urllib3": { "hashes": [ - "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", - "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" + "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", + "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" ], "markers": "python_version >= '3.8'", - "version": "==2.2.1" + "version": "==2.2.2" } } } diff --git a/base/fixtures/flatpages.json b/base/fixtures/flatpages.json index 11e88bb9..e9d9da92 100644 --- a/base/fixtures/flatpages.json +++ b/base/fixtures/flatpages.json @@ -51,7 +51,7 @@ }, { "model": "flatpages.flatpage", - "pk": 3, + "pk": 4, "fields": { "url": "kontakty", "title": "Kontakty", @@ -65,5 +65,22 @@ 2 ] } + }, + { + "model": "flatpages.flatpage", + "pk": 5, + "fields": { + "url": "ako-riesit", + "title": "Ako získať 9 bodov", + "content": "Tu pribudnú tipy, ako riešiť a získať 9 bodov.", + "enable_comments": false, + "template_name": "", + "registration_required": false, + "sites": [ + 0, + 1, + 2 + ] + } } ] \ No newline at end of file diff --git a/cms/migrations/0001_initial.py b/cms/migrations/0001_initial.py new file mode 100644 index 00000000..98a0ea83 --- /dev/null +++ b/cms/migrations/0001_initial.py @@ -0,0 +1,105 @@ +# Generated by Django 4.2.13 on 2024-06-10 20:10 + +import base.models +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('sites', '0002_alter_domain_unique'), + ] + + operations = [ + migrations.CreateModel( + name='InfoBanner', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('visible_after', models.DateTimeField(verbose_name='Zobrazuj od')), + ('visible_until', models.DateTimeField(verbose_name='Zobrazuj do')), + ('message', models.CharField(help_text='Správa sa zobrazí v baneri. Správa musí byť stručná - jedna krátka veta.', max_length=200, verbose_name='správa')), + ], + options={ + 'verbose_name': 'Informácia v pohyblivom baneri', + 'verbose_name_plural': 'Informácie v pohyblivom baneri', + }, + ), + migrations.CreateModel( + name='Logo', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=150, verbose_name='názov loga')), + ('disabled', models.BooleanField()), + ('image', base.models.RestrictedFileField(upload_to='logo_images/', verbose_name='Logo')), + ], + options={ + 'verbose_name': 'logo', + 'verbose_name_plural': 'logá', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='MessageTemplate', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='Pomenovanie generickej správy. Slúži pre orientáciu', max_length=50, verbose_name='Názov')), + ('message', models.CharField(help_text='Generické správy pre banner a posty', max_length=200, verbose_name='Generická správa')), + ('is_active', models.BooleanField(default=True, verbose_name='Aktívna')), + ], + options={ + 'verbose_name': 'Generické správy pre banner a posty', + 'verbose_name_plural': 'Generické správy pre banner a posty', + }, + ), + migrations.CreateModel( + name='Post', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('visible_after', models.DateTimeField(verbose_name='Zobrazuj od')), + ('visible_until', models.DateTimeField(verbose_name='Zobrazuj do')), + ('caption', models.CharField(max_length=50, verbose_name='nadpis')), + ('short_text', models.CharField(help_text='Krátky 1-2 vetový popis.', max_length=200, verbose_name='krátky text')), + ('details', models.TextField(blank=True, help_text='Dlhší text, ktorý sa zobrazí po rozkliknutí.', verbose_name='podrobnosti k príspevku')), + ('added_at', models.DateTimeField(auto_now_add=True, verbose_name='pridané')), + ('sites', models.ManyToManyField(to='sites.site')), + ], + options={ + 'verbose_name': 'príspevok', + 'verbose_name_plural': 'príspevky', + 'ordering': ['-added_at'], + }, + ), + migrations.CreateModel( + name='PostLink', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('caption', models.CharField(help_text='Nápis, ktorý po kliknutí presmeruje na link. Maximálne 2 slová.', max_length=25, verbose_name='názov')), + ('url', models.CharField(help_text='URL stránky kam má preklik viesť', max_length=100, verbose_name='URL')), + ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='links', to='cms.post', verbose_name='Relevantný príspevok')), + ], + options={ + 'verbose_name': 'link k príspevku', + 'verbose_name_plural': 'linky k príspevkom', + }, + ), + migrations.CreateModel( + name='MenuItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('caption', models.CharField(help_text='Nápis, ktorý sa zobrazí v menu. Maximálne 2 slová.', max_length=25, verbose_name='názov')), + ('url', models.CharField(help_text='URL stránky kam má preklik viesť', max_length=100, verbose_name='URL')), + ('priority', models.SmallIntegerField(help_text='Priorita, čím väčšie, tým vyššie v menu.', verbose_name='priorita')), + ('in_footer', models.BooleanField(default=False, verbose_name='Je v pätičke')), + ('in_menu', models.BooleanField(default=True, verbose_name='Je v menu')), + ('sites', models.ManyToManyField(to='sites.site')), + ], + options={ + 'verbose_name': 'položka v menu', + 'verbose_name_plural': 'položky v menu', + 'ordering': ['-priority'], + }, + ), + ] diff --git a/cms/migrations/0002_initial.py b/cms/migrations/0002_initial.py new file mode 100644 index 00000000..22145826 --- /dev/null +++ b/cms/migrations/0002_initial.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.13 on 2024-06-10 20:10 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('flatpages', '0001_initial'), + ('competition', '0001_initial'), + ('cms', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='infobanner', + name='event', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='competition.event'), + ), + migrations.AddField( + model_name='infobanner', + name='message_template', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='cms.messagetemplate'), + ), + migrations.AddField( + model_name='infobanner', + name='page', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='flatpages.flatpage'), + ), + migrations.AddField( + model_name='infobanner', + name='series', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='competition.series'), + ), + ] diff --git a/cms/models.py b/cms/models.py index 7f077881..d0d0598b 100644 --- a/cms/models.py +++ b/cms/models.py @@ -141,11 +141,14 @@ class Meta: verbose_name='správa', help_text='Správa sa zobrazí v baneri. Správa musí byť stručná - jedna krátka veta.', max_length=200) - page = models.ForeignKey(FlatPage, on_delete=models.CASCADE, null=True) - event = models.ForeignKey(Event, on_delete=models.CASCADE, null=True) - series = models.ForeignKey(Series, on_delete=models.CASCADE, null=True) + page = models.ForeignKey( + FlatPage, on_delete=models.CASCADE, null=True, blank=True) + event = models.ForeignKey( + Event, on_delete=models.CASCADE, null=True, blank=True) + series = models.ForeignKey( + Series, on_delete=models.CASCADE, null=True, blank=True) message_template = models.ForeignKey( - MessageTemplate, on_delete=models.PROTECT, null=True) + MessageTemplate, on_delete=models.PROTECT, null=True, blank=True) def clean(self): try: diff --git a/cms/views.py b/cms/views.py index 9f7fdb94..a7590337 100644 --- a/cms/views.py +++ b/cms/views.py @@ -1,5 +1,8 @@ +from datetime import datetime + +from django.utils.timezone import now from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.request import Request @@ -10,6 +13,7 @@ from cms.serializers import (InfoBannerSerializer, LogoSerializer, MenuItemShortSerializer, MessageTemplateSerializer, PostSerializer) +from competition.models import Competition, Event, Series class MenuItemViewSet(viewsets.ReadOnlyModelViewSet): @@ -62,6 +66,66 @@ class InfoBannerViewSet(viewsets.ModelViewSet): queryset = InfoBanner.objects.visible() filterset_fields = ['event', 'page', 'series'] + def format_date(self, datetime_: datetime): + return datetime_.strftime("%d.%m.%Y %H:%M") + + @action(methods=['get'], detail=False, url_path=r'series-problems/(?P\d+)') + def series_problems(self, request, series_id: int) -> list[str]: + series_messages = InfoBanner.objects.filter(series=series_id).all() + messages = [message.render_message() for message in series_messages] + series = Series.objects.get(pk=series_id) + if series.complete: + messages.append('Séria je uzavretá') + elif series.can_submit: + messages.append( + f'Termín série: {self.format_date(series.deadline)}' + ) + else: + messages.append('Prebieha opravovanie') + return Response(messages) + + @action(methods=['get'], detail=False, url_path=r'series-results/(?P\d+)') + def series_results(self, request, series_id): + series = Series.objects.get(pk=series_id) + if not series.complete: + return Response(['Poradie nie je uzavreté']) + return Response([]) + + @action(methods=['get'], detail=False, url_path=r'competition/(?P\d+)') + def event(self, request, competition_id: int) -> list[str]: + competition = Competition.objects.get(pk=competition_id) + try: + event = Event.objects.filter( + competition=competition_id, end__gte=now()).earliest('start') + except Event.DoesNotExist: + return Response([]) + event_messages = InfoBanner.objects.filter(event=event).all() + messages = [message.render_message() for message in event_messages] + + if event.registration_link is not None: + if competition.competition_type.name == 'Seminár': + if event.registration_link.start > now(): + messages.append( + 'Prihlasovanie na sústredenie bude spustené ' + f'{self.format_date(event.registration_link.start)}') + elif event.registration_link.end > now(): + messages.append( + 'Prihlasovanie na sústredenie končí ' + f'{self.format_date(event.registration_link.end)}') + else: + if event.registration_link.start > now(): + messages.append( + 'Registrácia bude spustená ' + f'{self.format_date(event.registration_link.start)}') + elif event.registration_link.end > now(): + messages.append( + 'Registrácia bude uzavretá ' + f'{self.format_date(event.registration_link.end)}') + + else: + messages.append('Registrácia ukončená') + return Response(messages) + class MessageTemplateViewSet(viewsets.ModelViewSet): """Templaty správ pre info banner/posty""" diff --git a/competition/admin.py b/competition/admin.py index 2a0451f1..3151a58d 100644 --- a/competition/admin.py +++ b/competition/admin.py @@ -3,7 +3,8 @@ from competition.models import (Comment, Competition, Event, EventRegistration, LateTag, Problem, ProblemCorrection, - Publication, Semester, Series, Solution) + Publication, RegistrationLink, Semester, + Series, Solution) @admin.register(Competition) @@ -187,6 +188,15 @@ class EventRegistrationAdmin(admin.ModelAdmin): ) +@admin.register(RegistrationLink) +class RegistrationLinkAdmin(admin.ModelAdmin): + list_display = ( + 'event', + 'start', + 'end' + ) + + @admin.register(ProblemCorrection) class ProblemCorrectionAdmin(admin.ModelAdmin): list_display = ( diff --git a/competition/fixtures/competition_types.json b/competition/fixtures/competition_types.json index 3284b462..703cb0ad 100644 --- a/competition/fixtures/competition_types.json +++ b/competition/fixtures/competition_types.json @@ -3,42 +3,48 @@ "model": "competition.CompetitionType", "pk": 0, "fields": { - "name": "Seminár" + "name": "Seminár", + "short_name": "seminár" } }, { "model": "competition.CompetitionType", "pk": 1, "fields": { - "name": "Jednodňová tímová súťaž" + "name": "Jednodňová tímová súťaž", + "short_name": "súťaž" } }, { "model": "competition.CompetitionType", "pk": 2, "fields": { - "name": "Tábor" + "name": "Tábor", + "short_name": "tábor" } }, { "model": "competition.CompetitionType", "pk": 3, "fields": { - "name": "Šifrovacia hra" + "name": "Šifrovacia hra", + "short_name": "šifrovačka" } }, { "model": "competition.CompetitionType", "pk": 4, "fields": { - "name": "Robotická tímová súťaž" + "name": "Robotická tímová súťaž", + "short_name": "súťaž" } }, { "model": "competition.CompetitionType", "pk": 5, "fields": { - "name": "Individuálna online súťaž" + "name": "Individuálna online súťaž", + "short_name": "súťaž" } } ] \ No newline at end of file diff --git a/competition/fixtures/events.json b/competition/fixtures/events.json index e5d47d17..8c20d287 100644 --- a/competition/fixtures/events.json +++ b/competition/fixtures/events.json @@ -7,7 +7,8 @@ "year": 19, "school_year": "2021/2022", "start": "2022-06-01T20:00:00+02:00", - "end": "2022-06-02T20:00:00+02:00" + "end": "2022-06-02T20:00:00+02:00", + "location": "v Košiciach" } }, { @@ -41,7 +42,8 @@ "school_year": "2022/2023", "start": "2023-06-01T20:00:00+02:00", "end": "2025-06-01T20:00:00+02:00", - "registration_link": 3 + "registration_link": 3, + "location": "v Košiciach" } } diff --git a/competition/migrations/0001_initial.py b/competition/migrations/0001_initial.py new file mode 100644 index 00000000..2c58a262 --- /dev/null +++ b/competition/migrations/0001_initial.py @@ -0,0 +1,238 @@ +# Generated by Django 4.2.13 on 2024-06-10 20:10 + +import base.models +import base.validators +import competition.models +import django.core.files.storage +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import re + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Comment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('text', models.TextField()), + ('posted_at', models.DateTimeField(auto_now_add=True, verbose_name='dátum pridania')), + ('state', models.IntegerField(choices=[(1, 'čaká'), (2, 'zverejnený'), (3, 'skrytý')], default=1, verbose_name='komentár publikovaný')), + ('hidden_response', models.TextField(blank=True, null=True, verbose_name='Skrytá odpoveď na komentár')), + ], + options={ + 'verbose_name': 'komentár', + 'verbose_name_plural': 'komentáre', + 'ordering': ['posted_at'], + }, + ), + migrations.CreateModel( + name='Competition', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50, verbose_name='názov')), + ('slug', models.SlugField()), + ('start_year', models.PositiveSmallIntegerField(blank=True, verbose_name='rok prvého ročníka súťaže')), + ('description', models.TextField(blank=True, verbose_name='Popis súťaže')), + ('rules', models.TextField(blank=True, null=True, verbose_name='Pravidlá súťaže')), + ('who_can_participate', models.CharField(blank=True, max_length=50, verbose_name='Pre koho je súťaž určená')), + ('min_years_until_graduation', models.PositiveSmallIntegerField(help_text='Horná hranica na účasť v súťaži. Zadáva sa v počte rokov do maturity. Ak najstraší, kto môže riešiť súťaž je deviatak, zadá sa 4.', null=True, verbose_name='Minimálny počet rokov do maturity')), + ], + options={ + 'verbose_name': 'súťaž', + 'verbose_name_plural': 'súťaže', + }, + ), + migrations.CreateModel( + name='CompetitionType', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200, verbose_name='typ súťaže')), + ('short_name', models.CharField(max_length=32, verbose_name='Krátky jednoslovný názov')), + ], + options={ + 'verbose_name': 'Typ súťaže', + 'verbose_name_plural': 'Typy súťaží', + }, + ), + migrations.CreateModel( + name='Event', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('year', models.PositiveSmallIntegerField(blank=True, verbose_name='ročník')), + ('school_year', models.CharField(blank=True, max_length=10, validators=[base.validators.school_year_validator], verbose_name='školský rok')), + ('season_code', models.PositiveSmallIntegerField(choices=[(0, 'Zimný'), (1, 'Letný'), (2, '')], default=2)), + ('start', models.DateTimeField(verbose_name='dátum začiatku súťaže')), + ('end', models.DateTimeField(verbose_name='dátum konca súťaže')), + ('location', models.TextField(blank=True, help_text='Napríklad "v Košiciach"', null=True, verbose_name='Miesto konania')), + ('additional_name', models.CharField(blank=True, max_length=50, null=True, verbose_name='Prívlastok súťaže')), + ], + options={ + 'verbose_name': 'ročník súťaže', + 'verbose_name_plural': 'ročníky súťaží', + 'ordering': ['-school_year'], + }, + ), + migrations.CreateModel( + name='EventRegistration', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + options={ + 'verbose_name': 'registrácia užívateľa na akciu', + 'verbose_name_plural': 'registrácie užívateľov na akcie', + }, + ), + migrations.CreateModel( + name='Grade', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=256, verbose_name='názov ročníku')), + ('tag', models.CharField(max_length=2, unique=True, verbose_name='skratka')), + ('years_until_graduation', models.SmallIntegerField(verbose_name='počet rokov do maturity')), + ('is_active', models.BooleanField(verbose_name='aktuálne používaný ročník')), + ], + options={ + 'verbose_name': 'ročník účastníka', + 'verbose_name_plural': 'ročníky účastníka', + 'ordering': ['years_until_graduation'], + }, + ), + migrations.CreateModel( + name='LateTag', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50, verbose_name='označenie štítku pre riešiteľa')), + ('slug', models.CharField(max_length=50, validators=[django.core.validators.RegexValidator(re.compile('^[-a-zA-Z0-9_]+\\Z'), 'Enter a valid “slug” consisting of letters, numbers, underscores or hyphens.', 'invalid')], verbose_name='označenie priečinku pri stiahnutí')), + ('upper_bound', models.DurationField(verbose_name='maximálna dĺžka omeškania')), + ('comment', models.TextField(verbose_name='komentár pre opravovateľa')), + ('can_resubmit', models.BooleanField(verbose_name='Možnosť prepísať odovzdané riešenie')), + ], + options={ + 'verbose_name': 'omeškanie', + 'verbose_name_plural': 'omeškanie', + }, + ), + migrations.CreateModel( + name='Problem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('text', models.TextField(verbose_name='znenie úlohy')), + ('order', models.PositiveSmallIntegerField(verbose_name='poradie v sérii')), + ('image', models.ImageField(blank=True, null=True, upload_to='problem_images/', verbose_name='Obrázok k úlohe')), + ('solution_pdf', base.models.RestrictedFileField(blank=True, null=True, upload_to='model_solutions/', verbose_name='Vzorové riešenie')), + ], + options={ + 'verbose_name': 'úloha', + 'verbose_name_plural': 'úlohy', + 'ordering': ['series', 'order'], + }, + ), + migrations.CreateModel( + name='PublicationType', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, verbose_name='názov typu')), + ], + options={ + 'verbose_name': 'Typ publikácie', + 'verbose_name_plural': 'Typy publikácií', + }, + ), + migrations.CreateModel( + name='RegistrationLink', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('url', models.URLField(verbose_name='url registrácie')), + ('start', models.DateTimeField(verbose_name='Začiatok registrácie')), + ('end', models.DateTimeField(verbose_name='Koniec registrácie')), + ('additional_info', models.TextField(blank=True, null=True, verbose_name='Doplňujúce informácie')), + ], + options={ + 'verbose_name': 'link na registráciu', + 'verbose_name_plural': 'linky na registráciu', + }, + ), + migrations.CreateModel( + name='Series', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order', models.PositiveSmallIntegerField(verbose_name='poradie série')), + ('deadline', models.DateTimeField(verbose_name='termín série')), + ('sum_method', models.CharField(blank=True, choices=[('series_simple_sum', 'Jednoduchý súčet bodov'), ('series_Malynar_sum', 'Bonifikácia Malynár'), ('series_Matik_sum', 'Bonifikácia Matik'), ('series_STROM_sum', 'Bonifikácia STROM'), ('series_Malynar_sum_until_2021', 'Bonifikácia Malynár (Do 2020/2021)'), ('series_Matik_sum_until_2021', 'Bonifikácia Matik (Do 2020/2021)'), ('series_STROM_sum_until_2021', 'Bonifikácia STROM (Do 2020/2021)'), ('series_STROM_4problems_sum', 'Bonifikácia STROM (4. úlohy)')], max_length=50, verbose_name='Súčtová metóda')), + ('frozen_results', models.TextField(blank=True, default=None, null=True)), + ], + options={ + 'verbose_name': 'séria', + 'verbose_name_plural': 'série', + 'ordering': ['semester', '-order'], + }, + ), + migrations.CreateModel( + name='Semester', + fields=[ + ('event_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='competition.event')), + ('frozen_results', models.TextField(blank=True, default=None, null=True)), + ], + options={ + 'verbose_name': 'semester', + 'verbose_name_plural': 'semestre', + 'ordering': ['-year', '-season_code'], + }, + bases=('competition.event',), + ), + migrations.CreateModel( + name='Solution', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('solution', base.models.RestrictedFileField(blank=True, storage=django.core.files.storage.FileSystemStorage(base_url='/protected/', location='/home/mihal/Documents/STROM/webstrom/webstrom-backend/protected_media/'), upload_to=competition.models.get_solution_path, verbose_name='účastnícke riešenie')), + ('corrected_solution', base.models.RestrictedFileField(blank=True, storage=django.core.files.storage.FileSystemStorage(base_url='/protected/', location='/home/mihal/Documents/STROM/webstrom/webstrom-backend/protected_media/'), upload_to=competition.models.get_corrected_solution_path, verbose_name='opravené riešenie')), + ('score', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='body')), + ('vote', models.IntegerField(choices=[(-1, 'negatívny'), (0, 'žiaden'), (1, 'pozitívny')], default=0)), + ('uploaded_at', models.DateTimeField(auto_now_add=True, verbose_name='dátum pridania')), + ('is_online', models.BooleanField(default=False, verbose_name='internetové riešenie')), + ('late_tag', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='competition.latetag', verbose_name='Stavy omeškania')), + ('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.problem')), + ('semester_registration', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.eventregistration')), + ], + options={ + 'verbose_name': 'riešenie', + 'verbose_name_plural': 'riešenia', + }, + ), + migrations.CreateModel( + name='Publication', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(blank=True, max_length=30)), + ('file', base.models.RestrictedFileField(upload_to='publications/%Y', verbose_name='súbor')), + ('order', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='poradie')), + ('event', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='competition.event')), + ('publication_type', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='competition.publicationtype')), + ], + options={ + 'verbose_name': 'Publikácia', + 'verbose_name_plural': 'Publikácie', + 'ordering': ['order'], + }, + ), + migrations.CreateModel( + name='ProblemCorrection', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('correct_solution_text', models.TextField(verbose_name='vzorák')), + ('best_solution', models.ManyToManyField(to='competition.solution', verbose_name='najkrajšie riešenia')), + ], + options={ + 'verbose_name': 'opravenie úlohy', + 'verbose_name_plural': 'opravene ulohy', + }, + ), + ] diff --git a/competition/migrations/0002_initial.py b/competition/migrations/0002_initial.py new file mode 100644 index 00000000..ba03d52b --- /dev/null +++ b/competition/migrations/0002_initial.py @@ -0,0 +1,105 @@ +# Generated by Django 4.2.13 on 2024-06-10 20:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('personal', '0002_initial'), + ('competition', '0001_initial'), + ('sites', '0002_alter_domain_unique'), + ('auth', '0012_alter_user_first_name_max_length'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='problemcorrection', + name='corrected_by', + field=models.ManyToManyField(to=settings.AUTH_USER_MODEL, verbose_name='opravovatelia'), + ), + migrations.AddField( + model_name='problemcorrection', + name='problem', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='correction', to='competition.problem'), + ), + migrations.AddField( + model_name='problem', + name='series', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='problems', to='competition.series', verbose_name='úloha zaradená do série'), + ), + migrations.AddField( + model_name='eventregistration', + name='event', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.event', verbose_name='semester'), + ), + migrations.AddField( + model_name='eventregistration', + name='grade', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.grade', verbose_name='ročník'), + ), + migrations.AddField( + model_name='eventregistration', + name='profile', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='personal.profile', verbose_name='profil'), + ), + migrations.AddField( + model_name='eventregistration', + name='school', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='personal.school', verbose_name='škola'), + ), + migrations.AddField( + model_name='event', + name='competition', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.competition'), + ), + migrations.AddField( + model_name='event', + name='registration_link', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='competition.registrationlink'), + ), + migrations.AddField( + model_name='competition', + name='competition_type', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='competition.competitiontype', verbose_name='typ súťaže'), + ), + migrations.AddField( + model_name='competition', + name='permission_group', + field=models.ManyToManyField(blank=True, related_name='competition_permissions', to='auth.group', verbose_name='Skupiny práv'), + ), + migrations.AddField( + model_name='competition', + name='sites', + field=models.ManyToManyField(to='sites.site'), + ), + migrations.AddField( + model_name='comment', + name='posted_by', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='autor komentára'), + ), + migrations.AddField( + model_name='comment', + name='problem', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.problem', verbose_name='komentár k úlohe'), + ), + migrations.AddField( + model_name='series', + name='semester', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.semester', verbose_name='semester'), + ), + migrations.AddField( + model_name='semester', + name='late_tags', + field=models.ManyToManyField(blank=True, to='competition.latetag', verbose_name='Stavy omeškania'), + ), + migrations.AddConstraint( + model_name='eventregistration', + constraint=models.UniqueConstraint(fields=('profile', 'event'), name='single_registration_in_event'), + ), + ] diff --git a/competition/models.py b/competition/models.py index 3a5e1ea1..8af195ff 100644 --- a/competition/models.py +++ b/competition/models.py @@ -1,4 +1,5 @@ import datetime +import json from typing import Optional from django.conf import settings @@ -19,12 +20,14 @@ from base.validators import school_year_validator from competition.exceptions import FreezingNotClosedResults from competition.querysets import ActiveQuerySet -from competition.utils.school_year_manipulation import ( - get_school_year_end_by_date) +from competition.utils.school_year_manipulation import \ + get_school_year_end_by_date from personal.models import Profile, School from user.models import User -private_storage = FileSystemStorage(location=settings.PRIVATE_STORAGE_ROOT) +private_storage = FileSystemStorage(location=settings.PRIVATE_STORAGE_ROOT, + base_url='/protected/' + ) class CompetitionType(models.Model): @@ -34,6 +37,8 @@ class Meta: verbose_name_plural = 'Typy súťaží' name = models.CharField('typ súťaže', max_length=200) + short_name = models.CharField( + verbose_name='Krátky jednoslovný názov', max_length=32) def __str__(self): return self.name @@ -159,6 +164,8 @@ class Meta: start = models.DateTimeField(verbose_name='dátum začiatku súťaže') end = models.DateTimeField(verbose_name='dátum konca súťaže') + location = models.TextField( + verbose_name='Miesto konania', help_text='Napríklad "v Košiciach"', null=True, blank=True) additional_name = models.CharField( max_length=50, verbose_name='Prívlastok súťaže', null=True, blank=True) @@ -166,6 +173,7 @@ class Meta: "competition.RegistrationLink", on_delete=models.SET_NULL, null=True, + blank=True ) objects = ActiveQuerySet.as_manager() @@ -244,7 +252,7 @@ def get_second_series(self) -> 'Series': def freeze_results(self, results): if any(not series.complete for series in self.series_set.all()): raise FreezingNotClosedResults() - self.frozen_results = results + self.frozen_results = json.dumps(results) @property def complete(self) -> bool: @@ -361,7 +369,7 @@ def freeze_results(self, results): for problem in self.problems.all() ): raise FreezingNotClosedResults() - self.frozen_results = results + self.frozen_results = json.dumps(results) @property def num_problems(self) -> int: @@ -535,7 +543,7 @@ class Meta: verbose_name_plural = 'ročníky účastníka' ordering = ['years_until_graduation', ] - name = models.CharField(verbose_name='názov ročníku', max_length=32) + name = models.CharField(verbose_name='názov ročníku', max_length=256) tag = models.CharField(verbose_name='skratka', max_length=2, unique=True) years_until_graduation = models.SmallIntegerField( verbose_name='počet rokov do maturity') @@ -619,6 +627,14 @@ class Vote(models.IntegerChoices): POSITIVE = 1, 'pozitívny' +def get_solution_path(instance, filename): # pylint: disable=unused-argument + return instance.get_solution_file_path() + + +def get_corrected_solution_path(instance, filename): # pylint: disable=unused-argument + return instance.get_corrected_solution_file_path() + + class Solution(models.Model): """ Popisuje riešenie úlohy od užívateľa. Obsahuje nahraté aj opravné riešenie, body @@ -634,11 +650,11 @@ class Meta: solution = RestrictedFileField( content_types=['application/pdf'], storage=private_storage, - verbose_name='účastnícke riešenie', blank=True, upload_to='solutions/user_solutions') + verbose_name='účastnícke riešenie', blank=True, upload_to=get_solution_path) corrected_solution = RestrictedFileField( content_types=['application/pdf'], storage=private_storage, - verbose_name='opravené riešenie', blank=True, upload_to='solutions/corrected/') + verbose_name='opravené riešenie', blank=True, upload_to=get_corrected_solution_path) score = models.PositiveSmallIntegerField( verbose_name='body', null=True, blank=True) @@ -664,13 +680,32 @@ def get_solution_file_name(self): return f'{self.semester_registration.profile.user.get_full_name_camel_case()}'\ f'-{self.problem.id}-{self.semester_registration.id}.pdf' + def get_solution_file_path(self): + return f'solutions/user_solutions/{self.get_solution_file_name()}' + def get_corrected_solution_file_name(self): - return f'corrected/{self.semester_registration.profile.user.get_full_name_camel_case()}'\ + return f'{self.semester_registration.profile.user.get_full_name_camel_case()}'\ f'-{self.problem.id}-{self.semester_registration.id}_corrected.pdf' + def get_corrected_solution_file_path(self): + return f'solutions/corrected/{self.get_corrected_solution_file_name()}' + def can_user_modify(self, user): return self.problem.can_user_modify(user) + def can_access(self, user): + return self.semester_registration.profile.user == user or self.can_user_modify(user) + + @classmethod + def get_by_filepath(cls, path): + try: + return cls.objects.get(solution=path) + except cls.DoesNotExist: + try: + return cls.objects.get(corrected_solution=path) + except cls.DoesNotExist: + return None + @classmethod def can_user_create(cls, user: User, data: dict) -> bool: problem = Problem.objects.get(pk=data['problem']) @@ -768,6 +803,9 @@ def can_user_modify(self, user): # pylint: disable=no-member return self.event.can_user_modify(user) + def __str__(self): + return str(self.event) # pylint: disable=no-member + class ProblemCorrection(models.Model): # TODO: Add images diff --git a/competition/serializers.py b/competition/serializers.py index 8e9d6438..9d04d201 100644 --- a/competition/serializers.py +++ b/competition/serializers.py @@ -123,14 +123,40 @@ def get_history_events(self, obj): @ts_interface(context='competition') -class EventRegistrationSerializer(serializers.ModelSerializer): +class GradeSerializer(serializers.ModelSerializer): + class Meta: + model = models.Grade + exclude = ['is_active'] + + +@ts_interface(context='competition') +class EventRegistrationReadSerializer(serializers.ModelSerializer): class Meta: model = models.EventRegistration - fields = ['school', 'grade', 'profile'] + fields = ['school', 'grade', 'profile', 'verbose_name', 'id', 'event'] school = SchoolShortSerializer(many=False) - grade = serializers.SlugRelatedField( - slug_field='tag', many=False, read_only=True) + grade = GradeSerializer(many=False) profile = ProfileShortSerializer(many=False) + verbose_name = serializers.SerializerMethodField('get_verbose_name') + + def get_verbose_name(self, obj): + return str(obj) + + +class EventRegistrationWriteSerializer(serializers.ModelSerializer): + class Meta: + model = models.EventRegistration + fields = ['school', 'grade', 'profile', 'id', 'event'] + + id = serializers.ReadOnlyField() + school = serializers.PrimaryKeyRelatedField( + queryset=models.School.objects.all()) + grade = serializers.PrimaryKeyRelatedField( + queryset=models.Grade.objects.all()) + profile = serializers.PrimaryKeyRelatedField( + queryset=models.Profile.objects.all()) + event = serializers.PrimaryKeyRelatedField( + queryset=models.Event.objects.all()) @ts_interface(context='competition') @@ -157,7 +183,7 @@ class ProblemSerializer(serializers.ModelSerializer): class Meta: model = models.Problem fields = '__all__' - read_only_fields = ['series', 'submitted', 'num_comments'] + read_only_fields = ['submitted', 'num_comments'] submitted = serializers.SerializerMethodField( 'get_submitted') @@ -221,7 +247,7 @@ class Meta: @ts_interface(context='competition') class SolutionAdministrationSerializer(serializers.ModelSerializer): - semester_registration = EventRegistrationSerializer(read_only=True) + semester_registration = EventRegistrationReadSerializer(read_only=True) class Meta: model = models.Solution @@ -361,6 +387,7 @@ class SemesterWithProblemsSerializer(ModelWithParticipationSerializer): ) publication_set = PublicationSerializer(many=True, read_only=True) complete = serializers.SerializerMethodField('get_complete') + verbose_name = serializers.SerializerMethodField('get_verbose_name') class Meta: model = models.Semester @@ -384,16 +411,12 @@ def create(self, validated_data: dict): semester.late_tags.add(tag) return semester + def get_verbose_name(self, obj): + return str(obj) + @ts_interface(context='competition') class LateTagSerializer(serializers.ModelSerializer): class Meta: model = models.LateTag exclude = ['comment'] - - -@ts_interface(context='competition') -class GradeSerializer(serializers.ModelSerializer): - class Meta: - model = models.Grade - exclude = ['is_active'] diff --git a/competition/templates/competition/emails/comment_added.txt b/competition/templates/competition/emails/comment_added.txt index f4458221..a598d911 100644 --- a/competition/templates/competition/emails/comment_added.txt +++ b/competition/templates/competition/emails/comment_added.txt @@ -1 +1,2 @@ -Bol pridaný nový komentár. +Bol pridaný nový komentár k úlohe {{problem}}: +{{comment}} diff --git a/competition/templates/competition/emails/comment_hidden.txt b/competition/templates/competition/emails/comment_hidden.txt index 5e11e571..e3cba46a 100644 --- a/competition/templates/competition/emails/comment_hidden.txt +++ b/competition/templates/competition/emails/comment_hidden.txt @@ -1 +1,4 @@ -Tvoj komentár bol skrytý a dostal si k nemu súkromnú odpoveď. +Tvoj komentár +{{comment}} +k úlohe {{problem}} bol skrytý a dostal si k nemu súkromnú odpoveď: +{{response}} diff --git a/competition/templates/competition/emails/comment_published.txt b/competition/templates/competition/emails/comment_published.txt index 2d9f7ad4..2f4e3f67 100644 --- a/competition/templates/competition/emails/comment_published.txt +++ b/competition/templates/competition/emails/comment_published.txt @@ -1 +1,3 @@ -Tvoj komentár bol zverejnený a dostal si k nemu odpoveď. +Tvoj komentár +{{comment}} +k úlohe {{problem}} bol zverejnený a dostal/a si k nemu odpoveď. diff --git a/competition/views.py b/competition/views.py index ef878cda..de93f33e 100644 --- a/competition/views.py +++ b/competition/views.py @@ -1,4 +1,5 @@ import csv +import json import os import zipfile from io import BytesIO @@ -9,6 +10,7 @@ from django.core.mail import send_mail from django.http import FileResponse, HttpResponse from django.template.loader import render_to_string +from django.utils.timezone import now from rest_framework import exceptions, mixins, status, viewsets from rest_framework.decorators import action from rest_framework.exceptions import ValidationError @@ -27,7 +29,8 @@ ProblemPermission) from competition.serializers import (CommentSerializer, CompetitionSerializer, CompetitionTypeSerializer, - EventRegistrationSerializer, + EventRegistrationReadSerializer, + EventRegistrationWriteSerializer, EventSerializer, GradeSerializer, LateTagSerializer, ProblemSerializer, ProblemWithSolutionsSerializer, @@ -42,7 +45,7 @@ rank_results) from personal.models import Profile, School from personal.serializers import ProfileExportSerializer, SchoolSerializer -from webstrom.settings import EMAIL_ALERT, EMAIL_NO_REPLY +from webstrom.settings import EMAIL_ALERT # pylint: disable=unused-argument @@ -78,7 +81,8 @@ def generate_result_row( solution_points.append(sol.score or 0 if sol is not None else 0) series_solutions.append( { - 'points': str(sol.score or '?') if sol is not None else '-', + 'points': (str(sol.score if sol.score is not None else '?') + if sol is not None else '-'), 'solution_pk': sol.pk if sol is not None else None, 'problem_pk': problem.pk, 'votes': 0 # TODO: Implement votes sol.vote @@ -98,7 +102,7 @@ def generate_result_row( # Indikuje či sa zmenilo poradie od minulej priečky, slúži na delené miesta 'rank_changed': True, # primary key riešiteľovej registrácie do semestra - 'registration': EventRegistrationSerializer(semester_registration).data, + 'registration': EventRegistrationReadSerializer(semester_registration).data, # Súčty bodov po sériách 'subtotal': subtotal, # Celkový súčet za danú entitu @@ -148,13 +152,18 @@ def get_serializer_context(self): @action(methods=['post'], detail=True) def publish(self, request, pk=None): """Publikovanie, teda zverejnenie komentára""" - comment = self.get_object() + comment: Comment = self.get_object() comment.publish() send_mail( 'Zverejnený komentár', - render_to_string('competition/emails/comment_published.txt'), - EMAIL_NO_REPLY, + render_to_string( + 'competition/emails/comment_published.txt', + context={ + 'comment': comment.text, + 'problem': comment.problem + }), + None, [comment.posted_by.email], ) @@ -165,13 +174,18 @@ def publish(self, request, pk=None): @action(methods=['post'], detail=True) def hide(self, request, pk=None): """Skrytie komentára""" - comment = self.get_object() + comment: Comment = self.get_object() comment.hide(message=request.data.get('hidden_response')) send_mail( 'Skrytý komentár', - render_to_string('competition/emails/comment_hidden.txt'), - EMAIL_NO_REPLY, + render_to_string('competition/emails/comment_hidden.txt', + context={ + 'comment': comment.text, + 'problem': comment.problem, + 'response': comment.hidden_response + }), + None, [comment.posted_by.email], ) @@ -202,7 +216,8 @@ def perform_create(self, serializer): Volá sa pri vytvarani objektu, checkuju sa tu permissions, ci user vie vytvorit problem v danej sutazi """ - if Problem.can_user_create(self.request.user, serializer.validated_data): + series = serializer.validated_data['series'] + if series.can_user_modify(self.request.user): serializer.save() else: raise exceptions.PermissionDenied( @@ -222,15 +237,19 @@ def comments(self, request, pk=None): permission_classes=[IsAuthenticated]) def add_comment(self, request, pk=None): """Pridá komentár (otázku) k úlohe""" - problem = self.get_object() + problem: Problem = self.get_object() also_publish = problem.can_user_modify(request.user) problem.add_comment(request.data['text'], request.user, also_publish) send_mail( 'Nový komentár', - render_to_string('competition/emails/comment_added.txt'), - EMAIL_NO_REPLY, + render_to_string('competition/emails/comment_added.txt', + context={ + 'problem': problem, + 'comment': request.data['text'] + }), + None, [EMAIL_ALERT], ) @@ -270,16 +289,20 @@ def upload_solution(self, request, pk=None): if len(existing_solutions) > 0 and late_tag is not None and not late_tag.can_resubmit: raise exceptions.ValidationError( detail='Túto úlohu už nie je možné odovzdať znova.') + for solution in existing_solutions: + solution.solution.delete() Solution.objects.filter( problem=problem, semester_registration=event_registration).delete() + solution = Solution.objects.create( problem=problem, semester_registration=event_registration, late_tag=late_tag, - is_online=True + is_online=True, + solution=file ) - solution.solution.save( - solution.get_solution_file_name(), file, save=True) + # solution.solution.save( + # solution.get_solution_file_name(), file, save=True) return Response(status=status.HTTP_201_CREATED) @@ -361,17 +384,17 @@ def upload_solutions_with_points(self, request, pk=None): """Nahrá .zip archív s opravenými riešeniami (pdf-kami).""" if 'file' not in request.data: - raise exceptions.ParseError(detail='No file attached') + raise exceptions.ParseError(detail='Žiaden súbor nebol pripojený') zfile = request.data['file'] if not zipfile.is_zipfile(zfile): raise exceptions.ParseError( - detail='Attached file is not a zip file') + detail='Priložený súbor nie je zip') with zipfile.ZipFile(zfile) as zfile: if zfile.testzip(): - raise exceptions.ParseError(detail='Zip file is corrupted') + raise exceptions.ParseError(detail='Súbor zip je poškodený') parsed_filenames = [] errors = [] @@ -390,20 +413,21 @@ def upload_solutions_with_points(self, request, pk=None): except (IndexError, ValueError, AssertionError): errors.append({ 'filename': filename, - 'status': 'Cannot parse file' + 'status': 'Nedá sa prečítať názov súboru. Skontroluj, že názov súboru' + 'je v tvare BODY-MENO-ID_ULOHY-ID_REGISTRACIE_USERA.pdf' }) continue except EventRegistration.DoesNotExist: errors.append({ 'filename': filename, - 'status': f'User registration with id {registration_pk} does not exist' + 'status': f'Registrácia používateľa s id {registration_pk} neexistuje' }) continue except Solution.DoesNotExist: errors.append({ 'filename': filename, - 'status': f'Solution with registration id {registration_pk}' - f'and problem id {problem_pk} does not exist' + 'status': f'Riešenie pre registráciu používateľa s id {registration_pk}' + f'a úlohy id {problem_pk} neexistuje' }) continue @@ -475,7 +499,7 @@ def results(self, request: Request, pk: Optional[int] = None): """Vráti výsledkovku pre sériu""" series = self.get_object() if series.frozen_results is not None: - return series.frozen_results + return Response(json.loads(series.frozen_results), status=status.HTTP_200_OK) results = self.__create_result_json(series) return Response(results, status=status.HTTP_200_OK) @@ -506,12 +530,17 @@ def stats(self, request, pk=None): @action(methods=['get'], detail=False, url_path=r'current/(?P\d+)') def current(self, request, competition_id=None): """Vráti aktuálnu sériu""" - items = Semester.objects.filter( + current_semester_series = Semester.objects.filter( competition=competition_id - ).current().series_set.filter(frozen_results__isnull=True)\ - .order_by('-deadline')\ - .first() - serializer = SeriesWithProblemsSerializer(items, many=False) + ).current().series_set + current_series = current_semester_series.filter( + deadline__gte=now() + ).order_by('deadline').first() + if current_series is None: + current_series = current_semester_series.order_by( + '-deadline').first() + serializer = SeriesWithProblemsSerializer( + current_series, many=False) return Response(serializer.data, status=status.HTTP_200_OK) @@ -578,7 +607,7 @@ def upload_solution_file(self, request, pk=None): raise exceptions.ParseError( detail='Riešenie nie je vo formáte pdf') solution.solution.save( - solution.get_solution_file_name(), file, save=True + solution.get_solution_file_path(), file, save=True ) return Response(status=status.HTTP_201_CREATED) @@ -595,7 +624,7 @@ def upload_corrected_solution_file(self, request, pk=None): raise exceptions.ParseError( detail='Riešenie nie je vo formáte pdf') solution.corrected_solution.save( - solution.get_corrected_solution_file_name(), file, save=True + solution.get_corrected_solution_file_path(), file, save=True ) return Response(status=status.HTTP_201_CREATED) @@ -633,7 +662,7 @@ def perform_create(self, serializer): def semester_results(semester): """Vyrobí výsledky semestra""" if semester.frozen_results is not None: - return semester.frozen_results + return json.loads(semester.frozen_results) results = [] for registration in semester.eventregistration_set.all(): results.append(generate_result_row(registration, semester)) @@ -769,7 +798,7 @@ def participants(self, request, pk=None): def participants_export(self, request, pk=None): """Vráti všetkých užívateľov zapojených do semestra""" serializer = self.__get_participants() - response = HttpResponse(content_type='text/csv') + response = HttpResponse(content_type='text/csv; charset=utf-8') response['Content-Disposition'] = 'attachment; filename="export.csv"' header = ProfileExportSerializer.Meta.fields writer = csv.DictWriter(response, fieldnames=header) @@ -857,10 +886,14 @@ def active(self, request): class EventRegistrationViewSet(viewsets.ModelViewSet): """Registrácie na akcie""" queryset = EventRegistration.objects.all() - serializer_class = EventRegistrationSerializer filterset_fields = ['event', 'profile', ] permission_classes = (CompetitionRestrictedPermission,) + def get_serializer_class(self): + if self.action in ['list', 'retrieve']: + return EventRegistrationReadSerializer + return EventRegistrationWriteSerializer + class PublicationTypeViewSet(viewsets.ReadOnlyModelViewSet): queryset = PublicationType.objects.all() diff --git a/compose.yaml b/compose.yaml deleted file mode 100644 index 6aed2794..00000000 --- a/compose.yaml +++ /dev/null @@ -1,9 +0,0 @@ -version: "3.3" - -services: - website: - build: . - image: webstrom-backend - ports: - - "8000:8000" - restart: always diff --git a/deployment/compose-test.yaml b/deployment/compose-test.yaml new file mode 100644 index 00000000..be492213 --- /dev/null +++ b/deployment/compose-test.yaml @@ -0,0 +1,32 @@ +version: "3" + +services: + webstrom-backend: + build: + dockerfile: deployment/webstrom-backend.dockerfile + context: .. + + image: webstrom-test-backend + + environment: + - DJANGO_SETTINGS_MODULE=webstrom.settings_test + + volumes: + - /var/run/postgresql:/var/run/postgresql:rw + + ports: + - "127.0.0.1:8920:8000" + + restart: always + + static-files: + build: + dockerfile: deployment/static-files.dockerfile + context: .. + + image: webstrom-test-static + + ports: + - "127.0.0.1:8921:80" + + restart: always diff --git a/deployment/static-files.dockerfile b/deployment/static-files.dockerfile new file mode 100644 index 00000000..89c9875d --- /dev/null +++ b/deployment/static-files.dockerfile @@ -0,0 +1,17 @@ +FROM python:3.11 AS static-files-builder + +WORKDIR /app + +COPY Pipfile /app +COPY Pipfile.lock /app + +RUN pip install pipenv +RUN pipenv sync --dev --system + +COPY . /app + +RUN python manage.py collectstatic --no-input + +FROM nginx:1.25 + +COPY --from=static-files-builder /app/static /usr/share/nginx/html diff --git a/Dockerfile b/deployment/webstrom-backend.dockerfile similarity index 52% rename from Dockerfile rename to deployment/webstrom-backend.dockerfile index bbe44070..02d9c4e6 100644 --- a/Dockerfile +++ b/deployment/webstrom-backend.dockerfile @@ -10,8 +10,4 @@ RUN pipenv sync --dev --system COPY . /app -RUN python manage.py restoredb - -EXPOSE 8000 - -ENTRYPOINT [ "daphne", "-b", "0.0.0.0", "-p", "8000", "webstrom.asgi:application" ] +CMD [ "gunicorn", "-b", "0.0.0.0", "-p", "8000", "webstrom.wsgi:application" ] diff --git a/problem_database/__init__.py b/downloads/__init__.py similarity index 100% rename from problem_database/__init__.py rename to downloads/__init__.py diff --git a/downloads/apps.py b/downloads/apps.py new file mode 100644 index 00000000..62b306e9 --- /dev/null +++ b/downloads/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class DownloadsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'downloads' diff --git a/problem_database/migrations/__init__.py b/downloads/migrations/__init__.py similarity index 100% rename from problem_database/migrations/__init__.py rename to downloads/migrations/__init__.py diff --git a/downloads/models.py b/downloads/models.py new file mode 100644 index 00000000..71a83623 --- /dev/null +++ b/downloads/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/downloads/tests.py b/downloads/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/downloads/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/downloads/urls.py b/downloads/urls.py new file mode 100644 index 00000000..4495d7f0 --- /dev/null +++ b/downloads/urls.py @@ -0,0 +1,16 @@ +from django.urls import re_path + +from competition.models import Solution + +from .views import download_protected_file + +urlpatterns = [ + # Include non-translated versions only since Admin ignores lang prefix + re_path(r'solutions/(?P.*)$', download_protected_file, + {'path_prefix': 'solutions/', 'model_class': Solution}, + name='download_solution'), + re_path(r'corrected_solutions/(?P.*)$', download_protected_file, + {'path_prefix': 'corrected_solutions/', + 'model_class': Solution}, + name='download_corrected_solution'), +] diff --git a/downloads/views.py b/downloads/views.py new file mode 100644 index 00000000..1986e1b6 --- /dev/null +++ b/downloads/views.py @@ -0,0 +1,29 @@ +import os + +from django.conf import settings +from django.contrib.auth.decorators import login_required +from django.core.exceptions import PermissionDenied +from django_sendfile import sendfile +from rest_framework.request import Request + + +@login_required +def download_protected_file(request: Request, model_class, path_prefix, path): + """ + This view allows download of the file at the specified path, if the user + is allowed to. This is checked by calling the model's can_access_files + method. + """ + filepath = os.path.join(settings.PRIVATE_STORAGE_ROOT, path_prefix, path) + filepath_mediapath = path_prefix + path + + if request.user.is_authenticated: + # Superusers can access all files + if request.user.is_superuser: + return sendfile(request, filepath) + obj = model_class.get_by_filepath(filepath_mediapath) + + if obj is not None and obj.can_access(request.user): + return sendfile(request, filepath) + + raise PermissionDenied diff --git a/personal/migrations/0001_initial.py b/personal/migrations/0001_initial.py new file mode 100644 index 00000000..66ae2f7b --- /dev/null +++ b/personal/migrations/0001_initial.py @@ -0,0 +1,72 @@ +# Generated by Django 4.2.13 on 2024-06-10 20:10 + +import django.core.validators +from django.db import migrations, models +import personal.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='County', + fields=[ + ('code', models.AutoField(primary_key=True, serialize=False, verbose_name='kód')), + ('name', models.CharField(max_length=30, verbose_name='názov')), + ], + options={ + 'verbose_name': 'kraj', + 'verbose_name_plural': 'kraje', + }, + ), + migrations.CreateModel( + name='District', + fields=[ + ('code', models.AutoField(primary_key=True, serialize=False, verbose_name='kód')), + ('name', models.CharField(max_length=30, verbose_name='názov')), + ('abbreviation', models.CharField(max_length=2, verbose_name='skratka')), + ], + options={ + 'verbose_name': 'okres', + 'verbose_name_plural': 'okresy', + }, + ), + migrations.CreateModel( + name='School', + fields=[ + ('code', models.AutoField(primary_key=True, serialize=False, verbose_name='kód')), + ('name', models.CharField(max_length=100, verbose_name='názov')), + ('abbreviation', models.CharField(max_length=10, verbose_name='skratka')), + ('street', models.CharField(max_length=100, verbose_name='ulica')), + ('city', models.CharField(max_length=100, verbose_name='obec')), + ('zip_code', models.CharField(max_length=6, verbose_name='PSČ')), + ('email', models.CharField(blank=True, max_length=50, verbose_name='email')), + ('district', models.ForeignKey(on_delete=models.SET(personal.models.unspecified_district), to='personal.district', verbose_name='okres')), + ], + options={ + 'verbose_name': 'škola', + 'verbose_name_plural': 'školy', + }, + ), + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('first_name', models.CharField(max_length=150, verbose_name='krstné meno')), + ('last_name', models.CharField(max_length=150, verbose_name='priezvisko')), + ('year_of_graduation', models.PositiveSmallIntegerField(verbose_name='rok maturity')), + ('phone', models.CharField(blank=True, help_text='Telefonné číslo v medzinárodnom formáte (napr. +421 123 456 789).', max_length=32, validators=[django.core.validators.RegexValidator(message='Zadaj telefónne číslo vo formáte +421 123 456 789 alebo 0912 345 678.', regex='^(\\+\\d{1,3}\\d{9})$')], verbose_name='telefónne číslo')), + ('parent_phone', models.CharField(blank=True, help_text='Telefonné číslo v medzinárodnom formáte (napr. +421 123 456 789).', max_length=32, validators=[django.core.validators.RegexValidator(message='Zadaj telefónne číslo vo formáte +421 123 456 789 alebo 0912 345 678.', regex='^(\\+\\d{1,3}\\d{9})$')], verbose_name='telefónne číslo na rodiča')), + ('school', models.ForeignKey(on_delete=models.SET(personal.models.unspecified_school), to='personal.school', verbose_name='škola')), + ], + options={ + 'verbose_name': 'profil', + 'verbose_name_plural': 'profily', + }, + ), + ] diff --git a/personal/migrations/0002_initial.py b/personal/migrations/0002_initial.py new file mode 100644 index 00000000..8778794b --- /dev/null +++ b/personal/migrations/0002_initial.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.13 on 2024-06-10 20:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import personal.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('personal', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='profile', + name='user', + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='district', + name='county', + field=models.ForeignKey(on_delete=models.SET(personal.models.unspecified_county), to='personal.county', verbose_name='kraj'), + ), + ] diff --git a/personal/models.py b/personal/models.py index 15488fd0..6b71574d 100644 --- a/personal/models.py +++ b/personal/models.py @@ -20,6 +20,10 @@ def __str__(self): return self.name +def unspecified_county(): + return County.objects.get_unspecified_value() + + class District(models.Model): class Meta: verbose_name = 'okres' @@ -31,7 +35,8 @@ class Meta: county = models.ForeignKey( County, verbose_name='kraj', - on_delete=models.SET(County.objects.get_unspecified_value)) + on_delete=models.SET(unspecified_county) + ) objects = UnspecifiedValueManager(unspecified_value_pk=0) @@ -39,6 +44,10 @@ def __str__(self): return self.name +def unspecified_district(): + return District.objects.get_unspecified_value() + + class School(models.Model): class Meta: verbose_name = 'škola' @@ -55,7 +64,8 @@ class Meta: district = models.ForeignKey( District, verbose_name='okres', - on_delete=models.SET(District.objects.get_unspecified_value)) + on_delete=models.SET(unspecified_district) + ) objects = UnspecifiedValueManager(unspecified_value_pk=0) @@ -74,6 +84,10 @@ def stitok(self): f'{{{ self.printable_zip_code }}}{{{ self.street }}}' +def unspecified_school(): + return School.objects.get_unspecified_value() + + class Profile(models.Model): class Meta: verbose_name = 'profil' @@ -92,8 +106,9 @@ class Meta: ) school = models.ForeignKey( - School, on_delete=models.SET(School.objects.get_unspecified_value), - verbose_name='škola') + School, on_delete=models.SET(unspecified_school), + verbose_name='škola' + ) year_of_graduation = models.PositiveSmallIntegerField( verbose_name='rok maturity') @@ -120,7 +135,7 @@ def grade(self, value): pk=value).get_year_of_graduation_by_date() def __str__(self): - return str(self.user) + return f'{self.full_name()} ({self.user})' def full_name(self): return f'{self.first_name} {self.last_name}' diff --git a/personal/serializers.py b/personal/serializers.py index e566f455..87832506 100644 --- a/personal/serializers.py +++ b/personal/serializers.py @@ -21,9 +21,20 @@ class Meta: @ts_interface(context='personal') class SchoolSerializer(serializers.ModelSerializer): + verbose_name = serializers.SerializerMethodField('get_verbose_name') + id = serializers.SerializerMethodField('get_id') + class Meta: model = School - fields = '__all__' + fields = ['id', 'code', 'name', 'abbreviation', 'street', + 'city', 'zip_code', 'email', 'district', 'verbose_name'] + read_only_fields = ['id', 'verbose_name'] + + def get_verbose_name(self, obj): + return str(obj) + + def get_id(self, obj): + return obj.code @ts_interface(context='personal') @@ -55,13 +66,15 @@ class ProfileSerializer(serializers.ModelSerializer): has_school = serializers.SerializerMethodField('get_has_school') school_id = serializers.IntegerField() email = serializers.EmailField(source='user.email') + verbose_name = serializers.SerializerMethodField('get_verbose_name') class Meta: model = Profile fields = ['grade_name', 'id', 'email', 'first_name', 'last_name', 'school', - 'phone', 'parent_phone', 'grade', 'is_student', 'has_school', 'school_id'] + 'phone', 'parent_phone', 'grade', 'is_student', 'has_school', + 'school_id', 'verbose_name'] read_only_fields = ['grade_name', 'id', 'first_name', 'last_name', - 'email', 'is_student', 'has_school', 'school'] # 'year_of_graduation', + 'email', 'is_student', 'has_school', 'school', 'verbose_name'] extra_kwargs = { 'grade': { @@ -72,6 +85,9 @@ class Meta: } } + def get_verbose_name(self, obj): + return str(obj) + def get_is_student(self, obj): return obj.school != School.objects.get(pk=1) @@ -149,7 +165,8 @@ def create(self, validated_data): class ProfileShortSerializer(serializers.ModelSerializer): class Meta: model = Profile - fields = ['first_name', 'last_name'] + fields = ['id', 'first_name', 'last_name'] + read_only_fields = ['id'] @ts_interface(context='personal') diff --git a/personal/views.py b/personal/views.py index 61dc107c..9c5e193c 100644 --- a/personal/views.py +++ b/personal/views.py @@ -32,7 +32,7 @@ class SchoolViewSet(viewsets.ModelViewSet): serializer_class = SchoolSerializer filterset_fields = ['district', 'district__county'] filter_backends = [DjangoFilterBackend, SearchFilter] - search_fields = ['name', 'street'] + search_fields = ['name', 'street', 'city'] def destroy(self, request, *args, **kwargs): """Zmazanie školy""" diff --git a/problem_database/admin.py b/problem_database/admin.py deleted file mode 100644 index 35cbcb85..00000000 --- a/problem_database/admin.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.contrib import admin - -from problem_database import models - - -@admin.register(models.ActivityType) -class ActivityTypeAdmin(admin.ModelAdmin): - list_display = ( - 'name', - 'seminar' - ) diff --git a/problem_database/apps.py b/problem_database/apps.py deleted file mode 100644 index b8426de6..00000000 --- a/problem_database/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class ProblemDatabaseConfig(AppConfig): - name = 'problem_database' diff --git a/problem_database/migrations/0001_initial.py b/problem_database/migrations/0001_initial.py new file mode 100644 index 00000000..186e8e6e --- /dev/null +++ b/problem_database/migrations/0001_initial.py @@ -0,0 +1,153 @@ +# Generated by Django 4.2.11 on 2024-04-14 09:24 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Activity', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateField(verbose_name='dátum')), + ('description', models.TextField(verbose_name='popis')), + ('soft_deleted', models.BooleanField(default=False)), + ], + options={ + 'verbose_name': 'aktivita', + 'verbose_name_plural': 'aktivity', + }, + ), + migrations.CreateModel( + name='ActivityType', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=64, verbose_name='názov')), + ], + options={ + 'verbose_name': 'typ aktivity', + 'verbose_name_plural': 'typy aktivít', + }, + ), + migrations.CreateModel( + name='Difficulty', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=128, verbose_name='názov')), + ('activity_type', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='problem_database.activitytype', verbose_name='typ aktivity')), + ], + options={ + 'verbose_name': 'náročnosť', + 'verbose_name_plural': 'náročnosti', + }, + ), + migrations.CreateModel( + name='Problem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('problem', models.TextField(verbose_name='príklad')), + ('result', models.CharField(max_length=128, verbose_name='výsledok')), + ('solution', models.TextField(verbose_name='riešenie')), + ('soft_deleted', models.BooleanField(default=False)), + ], + options={ + 'verbose_name': 'príklad', + 'verbose_name_plural': 'príklady', + }, + ), + migrations.CreateModel( + name='Seminar', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=32, verbose_name='názov seminára')), + ], + options={ + 'verbose_name': 'seminár', + 'verbose_name_plural': 'semináre', + }, + ), + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=64, verbose_name='názov tagu')), + ], + options={ + 'verbose_name': 'tag', + 'verbose_name_plural': 'tagy', + }, + ), + migrations.CreateModel( + name='ProblemType', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=64, verbose_name='názov')), + ('description', models.TextField(verbose_name='popis')), + ('seminar', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='problem_database.seminar', verbose_name='seminár')), + ], + options={ + 'verbose_name': 'typ príkladu', + 'verbose_name_plural': 'typy príkladov', + }, + ), + migrations.CreateModel( + name='ProblemTag', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('problem', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='problem_database.problem', verbose_name='príklad')), + ('tag', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='problem_database.tag', verbose_name='aktivita')), + ], + options={ + 'verbose_name': 'priradenie príkladu k tagu', + 'verbose_name_plural': 'priradenia príkladov k tagom', + }, + ), + migrations.CreateModel( + name='ProblemActivity', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('activity', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='problem_database.activity', verbose_name='aktivita')), + ('difficulty', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='problem_database.difficulty', verbose_name='náročnosť')), + ('problem', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='problem_database.problem', verbose_name='príklad')), + ], + options={ + 'verbose_name': 'priradenie problému k aktivite/obtiežnosti', + 'verbose_name_plural': 'priradenie problémov k aktivitám/obtiažnostiam', + }, + ), + migrations.AddField( + model_name='problem', + name='problem_type', + field=models.ManyToManyField(to='problem_database.problemtype'), + ), + migrations.CreateModel( + name='Media', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('data', models.ImageField(upload_to='', verbose_name='priložené súbory')), + ('soft_deleted', models.BooleanField(default=False)), + ('problem', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='problem_database.problem', verbose_name='príklad')), + ], + options={ + 'verbose_name': 'súbor', + 'verbose_name_plural': 'súbory', + }, + ), + migrations.AddField( + model_name='activitytype', + name='seminar', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='problem_database.seminar', verbose_name='seminár'), + ), + migrations.AddField( + model_name='activity', + name='activity_type', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='problem_database.activitytype', verbose_name='typ aktivity'), + ), + ] diff --git a/problem_database/models.py b/problem_database/models.py deleted file mode 100644 index 390ce37b..00000000 --- a/problem_database/models.py +++ /dev/null @@ -1,139 +0,0 @@ -from django.db import models - -# Create your models here. - - -class Seminar(models.Model): - class Meta: - verbose_name = 'seminár' - verbose_name_plural = 'semináre' - - name = models.CharField(max_length=32, verbose_name='názov seminára') - - def __str__(self): - return self.name - - -class ActivityType(models.Model): - class Meta: - verbose_name = 'typ aktivity' - verbose_name_plural = 'typy aktivít' - - name = models.CharField(verbose_name='názov', max_length=64) - seminar = models.ForeignKey( - Seminar, default=1, on_delete=models.CASCADE, verbose_name='seminár') - - def __str__(self): - return self.name - - -class Activity(models.Model): - class Meta: - verbose_name = 'aktivita' - verbose_name_plural = 'aktivity' - - # dátum je vo formáte YYYY-MM-DD - date = models.DateField(verbose_name='dátum') - activity_type = models.ForeignKey( - ActivityType, default=1, on_delete=models.CASCADE, verbose_name='typ aktivity') - description = models.TextField(verbose_name='popis') - soft_deleted = models.BooleanField(default=False) - - def __str__(self): - return self.description - - -class Difficulty(models.Model): - class Meta: - verbose_name = 'náročnosť' - verbose_name_plural = 'náročnosti' - - name = models.CharField(verbose_name='názov', max_length=128) - activity_type = models.ForeignKey( - ActivityType, default=1, on_delete=models.CASCADE, verbose_name='typ aktivity') - - def __str__(self): - return self.name - - -class ProblemType(models.Model): - class Meta: - verbose_name = 'typ príkladu' - verbose_name_plural = 'typy príkladov' - - seminar = models.ForeignKey( - Seminar, default=1, on_delete=models.CASCADE, verbose_name='seminár') - name = models.CharField(verbose_name='názov', max_length=64) - description = models.TextField(verbose_name='popis') - - def __str__(self): - return self.name - - -class Problem(models.Model): - class Meta: - verbose_name = 'príklad' - verbose_name_plural = 'príklady' - - problem = models.TextField(verbose_name='príklad') - result = models.CharField(verbose_name='výsledok', max_length=128) - solution = models.TextField(verbose_name='riešenie') - soft_deleted = models.BooleanField(default=False) - problem_type = models.ManyToManyField(ProblemType) - - def __str__(self): - return self.problem - - -class Media(models.Model): - class Meta: - verbose_name = 'súbor' - verbose_name_plural = 'súbory' - - data = models.ImageField(verbose_name='priložené súbory') - problem = models.ForeignKey( - Problem, default=1, on_delete=models.CASCADE, verbose_name='príklad') - soft_deleted = models.BooleanField(default=False) - - def __str__(self): - return f'{self.problem} - {self.data.name}' - - -class ProblemActivity(models.Model): - class Meta: - verbose_name = 'priradenie problému k aktivite/obtiežnosti' - verbose_name_plural = 'priradenie problémov k aktivitám/obtiažnostiam' - - problem = models.ForeignKey( - Problem, default=1, on_delete=models.CASCADE, verbose_name='príklad') - activity = models.ForeignKey( - Activity, default=1, on_delete=models.CASCADE, verbose_name='aktivita') - difficulty = models.ForeignKey( - Difficulty, default=1, on_delete=models.CASCADE, verbose_name='náročnosť') - - def __str__(self): - return f'{ self.problem }, { self.activity }, { self.difficulty }' - - -class Tag(models.Model): - class Meta: - verbose_name = 'tag' - verbose_name_plural = 'tagy' - name = models.CharField(max_length=64, verbose_name='názov tagu') - - def __str__(self): - return self.name - - -class ProblemTag(models.Model): - class Meta: - verbose_name = 'priradenie príkladu k tagu' - verbose_name_plural = 'priradenia príkladov k tagom' - - problem = models.ForeignKey( - Problem, default=1, on_delete=models.CASCADE, verbose_name='príklad') - tag = models.ForeignKey( - Tag, default=1, on_delete=models.CASCADE, verbose_name='aktivita') - - def __str__(self): - return f'{ self.problem }, { self.tag }' diff --git a/problem_database/serializers.py b/problem_database/serializers.py deleted file mode 100644 index 682f3eeb..00000000 --- a/problem_database/serializers.py +++ /dev/null @@ -1,65 +0,0 @@ -from django_typomatic import ts_interface -from rest_framework import serializers - -from problem_database import models - - -@ts_interface(context='problem_database') -class SeminarSerializer(serializers.ModelSerializer): - class Meta: - model = models.Seminar - fields = '__all__' - -@ts_interface(context='problem_database') -class ActivityTypeSerializer(serializers.ModelSerializer): - class Meta: - model = models.ActivityType - fields = '__all__' - -@ts_interface(context='problem_database') -class ActivitySerializer(serializers.ModelSerializer): - class Meta: - model = models.Activity - fields = '__all__' - -@ts_interface(context='problem_database') -class DifficultySerializer(serializers.ModelSerializer): - class Meta: - model = models.Difficulty - fields = '__all__' - -@ts_interface(context='problem_database') -class ProblemSerializer(serializers.ModelSerializer): - class Meta: - model = models.Problem - fields = '__all__' - -@ts_interface(context='problem_database') -class MediaSerializer(serializers.ModelSerializer): - class Meta: - model = models.Media - fields = '__all__' - -@ts_interface(context='problem_database') -class ProblemActivitySerializer(serializers.ModelSerializer): - class Meta: - model = models.ProblemActivity - fields = '__all__' - -@ts_interface(context='problem_database') -class ProblemTypeSerializer(serializers.ModelSerializer): - class Meta: - model = models.ProblemType - fields = '__all__' - -@ts_interface(context='problem_database') -class TagSerializer(serializers.ModelSerializer): - class Meta: - model = models.Tag - fields = '__all__' - -@ts_interface(context='problem_database') -class ProblemTagSerializer(serializers.ModelSerializer): - class Meta: - model = models.ProblemTag - fields = '__all__' diff --git a/problem_database/tests.py b/problem_database/tests.py deleted file mode 100644 index c54efad2..00000000 --- a/problem_database/tests.py +++ /dev/null @@ -1,214 +0,0 @@ -from django.test import TestCase -from rest_framework import status -from rest_framework.test import APITestCase - -from problem_database import models, serializers - - -class TestSeminar(TestCase): - ''' - seminar create - ''' - - def setUp(self): - return models.Seminar.objects.create(name='Malynár') - - def test_seminar_check_title(self): - mod = self.setUp() - self.assertTrue(isinstance(mod, models.Seminar)) - self.assertEqual(str(mod), 'Malynár') - - -class SeminarViewsTest(APITestCase): - ''' - prolem_database/seminars - ''' - - def setUp(self): - self.seminars = [models.Seminar.objects.create(name="Malynár"), - models.Seminar.objects.create(name="Matik"), - models.Seminar.objects.create(name="Strom")] - - URL_PREFIX = '/problem-database/seminars' - - def test_can_browse_all_seminars(self): - response = self.client.get(self.URL_PREFIX, {}, 'json') - - self.assertEqual(status.HTTP_200_OK, response.status_code) - self.assertEqual(len(self.seminars), len(response.data)) - - for seminar in self.seminars: - self.assertIn( - serializers.SeminarSerializer(instance=seminar).data, - response.data - ) - - -class TestActivityType(TestCase): - ''' - activity type create - ''' - - def setUp(self): - seminar = models.Seminar.objects.create(name='Malynár') - return models.ActivityType.objects.create(name='Mamut', seminar=seminar) - - def test_activity_type_check_title(self): - mod = self.setUp() - self.assertTrue(isinstance(mod, models.ActivityType)) - self.assertEqual(str(mod), 'Mamut') - - -class ActivityTypeViewsTest(APITestCase): - ''' - problem_database/activity_types - ''' - - def setUp(self): - self.seminar1 = models.Seminar.objects.create(name='Malynár') - self.seminar2 = models.Seminar.objects.create(name='Matik') - self.activity_types = [ - models.ActivityType.objects.create( - name="Malynár", seminar=self.seminar1), - models.ActivityType.objects.create( - name="Mamut", seminar=self.seminar1), - models.ActivityType.objects.create( - name="Lomihlav", seminar=self.seminar2)] - - URL_PREFIX = '/problem-database/activity-types' - - def test_can_browse_all_activity_types(self): - response = self.client.get(self.URL_PREFIX, {}, 'json') - - self.assertEqual(status.HTTP_200_OK, response.status_code) - self.assertEqual(len(self.activity_types), len(response.data)) - - for activity_type in self.activity_types: - self.assertIn( - serializers.ActivityTypeSerializer( - instance=activity_type).data, - response.data - ) - - def test_filter_activity_types(self): - response = self.client.get( - f'{self.URL_PREFIX}/?seminar={self.seminar2.pk}', {}, 'json') - - self.assertEqual(status.HTTP_200_OK, response.status_code) - self.assertEqual(1, len(response.data)) - - self.assertIn( - serializers.ActivityTypeSerializer( - instance=self.activity_types[2]).data, - response.data - ) - - response = self.client.get( - f'{self.URL_PREFIX}/?seminar={self.seminar1.pk}', {}, 'json') - - self.assertEqual(status.HTTP_200_OK, response.status_code) - self.assertEqual(2, len(response.data)) - - for activity_type in self.activity_types[:2]: - self.assertIn( - serializers.ActivityTypeSerializer( - instance=activity_type).data, - response.data - ) - - -class TestActivity(TestCase): - ''' - activity create - ''' - - def setUp(self): - seminar = models.Seminar.objects.create(name='Malynár') - activity_type = models.ActivityType.objects.create( - name='Mamut', seminar=seminar) - return models.Activity.objects.create( - date='2020-06-20', - activity_type=activity_type, - description='Mamut 2020') - - def test_activity_type_check_title(self): - mod = self.setUp() - self.assertTrue(isinstance(mod, models.Activity)) - self.assertEqual(str(mod), 'Mamut 2020') - - -class TestProblemTag(TestCase): - ''' - problem tag create - ''' - - def setUp(self): - problem = models.Problem.objects.create( - problem='Lorem?', result='Ipsum') - tag = models.Tag.objects.create(name='Výroková logika') - return models.ProblemTag.objects.create(problem=problem, tag=tag) - - def test_problem_tag_check_title(self): - mod = self.setUp() - self.assertTrue(isinstance(mod, models.ProblemTag)) - self.assertEqual(str(mod), 'Lorem?, Výroková logika') - - -class ProblemTagTest(APITestCase): - ''' - problem_database/problem_tags - ''' - - def setUp(self): - problems = [ - models.Problem.objects.create( - problem='Koľko je 1+2?', result='3', solution='indukciou'), - models.Problem.objects.create( - problem='Koľko je 2+3?', result='5', solution='priamo') - ] - tags = [ - models.Tag.objects.create(name='Indukcia'), - models.Tag.objects.create(name='Aritmetika') - ] - self.problem_tags = [ - models.ProblemTag.objects.create(problem=problems[0], tag=tags[0]), - models.ProblemTag.objects.create(problem=problems[0], tag=tags[1]), - models.ProblemTag.objects.create(problem=problems[1], tag=tags[1]) - ] - - URL_PREFIX = '/problem-database/problem-tags' - - def test_can_browse_all_problem_tags(self): - response = self.client.get(self.URL_PREFIX, {}, 'json') - - self.assertEqual(status.HTTP_200_OK, response.status_code) - self.assertEqual(len(self.problem_tags), len(response.data)) - - for problem_tag in self.problem_tags: - self.assertIn( - serializers.ProblemTagSerializer(instance=problem_tag).data, - response.data - ) - - def test_filter_problem_tags(self): - response = self.client.get(self.URL_PREFIX + '/?problem=1', {}, 'json') - - self.assertEqual(status.HTTP_200_OK, response.status_code) - self.assertEqual(2, len(response.data)) - - for problem_tag in self.problem_tags[:2]: - self.assertIn( - serializers.ProblemTagSerializer(instance=problem_tag).data, - response.data - ) - - response = self.client.get(self.URL_PREFIX + '/?tag=1', {}, 'json') - - self.assertEqual(status.HTTP_200_OK, response.status_code) - self.assertEqual(1, len(response.data)) - - self.assertIn( - serializers.ProblemTagSerializer( - instance=self.problem_tags[0]).data, - response.data - ) diff --git a/problem_database/urls.py b/problem_database/urls.py deleted file mode 100644 index bd3c6c27..00000000 --- a/problem_database/urls.py +++ /dev/null @@ -1,22 +0,0 @@ -from django.urls.conf import include, path -from rest_framework.routers import DefaultRouter - -from problem_database import views - -router = DefaultRouter() -router.register(r'seminars', views.SeminarViewSet) -router.register(r'activity-types', views.ActivityTypeViewSet) -router.register(r'activities', views.ActivityViewSet) -router.register(r'difficulty', views.DifficultyViewSet) -router.register(r'problems', views.ProblemViewSet) -router.register(r'media', views.ProblemViewSet) -router.register(r'problem-activities', views.ProblemActivityViewSet) -router.register(r'problem-types', views.ProblemTypeViewSet) -router.register(r'tags', views.TagViewSet) -router.register(r'problem-tags', views.ProblemTagViewSet) - -app_name = "problem_database" - -urlpatterns = [ - path('', include(router.urls)), -] diff --git a/problem_database/views.py b/problem_database/views.py deleted file mode 100644 index 5a12bf19..00000000 --- a/problem_database/views.py +++ /dev/null @@ -1,87 +0,0 @@ -# Create your views here. -from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import status, viewsets -from rest_framework.filters import SearchFilter -from rest_framework.response import Response - -from problem_database import models, serializers - -# Filterset umoznuju pouzit URL v tvare profile/districts/?county=1 -# Search filter umoznuju pouzit URL v tvare profile/schools/?search=Alej - - -class SeminarViewSet(viewsets.ReadOnlyModelViewSet): - queryset = models.Seminar.objects.all() - serializer_class = serializers.SeminarSerializer - - def create(self, request, *args, **kwargs): # pylint: disable=unused-argument - many = isinstance(request.data, list) - serializer = serializers.SeminarSerializer( - data=request.data, many=many) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -class ActivityTypeViewSet(viewsets.ReadOnlyModelViewSet): - queryset = models.ActivityType.objects.all() - serializer_class = serializers.ActivityTypeSerializer - filterset_fields = ['seminar', ] - - -class ActivityViewSet(viewsets.ReadOnlyModelViewSet): - queryset = models.Activity.objects.all() - serializer_class = serializers.ActivitySerializer - filterset_fields = ['activity_type', ] - - -class DifficultyViewSet(viewsets.ReadOnlyModelViewSet): - queryset = models.Difficulty.objects.all() - serializer_class = serializers.DifficultySerializer - - -class ProblemTypeViewSet(viewsets.ReadOnlyModelViewSet): - queryset = models.ProblemType.objects.all() - serializer_class = serializers.ProblemTypeSerializer - - -class ProblemViewSet(viewsets.ReadOnlyModelViewSet): - queryset = models.Problem.objects.all() - serializer_class = serializers.ProblemSerializer - filterset_fields = ['problem_type', ] - filter_backends = [DjangoFilterBackend, SearchFilter] - search_fields = ['problem'] - - def create(self, request, *args, **kwargs): # pylint: disable=unused-argument - many = isinstance(request.data, list) - serializer = serializers.ProblemSerializer( - data=request.data, many=many) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -class MediaViewSet(viewsets.ReadOnlyModelViewSet): - queryset = models.Media.objects.all() - serializer_class = serializers.MediaSerializer - filterset_fields = ['problem', ] - - -class ProblemActivityViewSet(viewsets.ReadOnlyModelViewSet): - queryset = models.ProblemActivity.objects.all() - serializer_class = serializers.ProblemActivitySerializer - - -class TagViewSet(viewsets.ReadOnlyModelViewSet): - queryset = models.Tag.objects.all() - serializer_class = serializers.TagSerializer - - -class ProblemTagViewSet(viewsets.ReadOnlyModelViewSet): - queryset = models.ProblemTag.objects.all() - serializer_class = serializers.ProblemTagSerializer - filterset_fields = ['problem', 'tag'] diff --git a/requirements.txt b/requirements.txt index d53a229f..0fc6ac5a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,62 +1,51 @@ asgiref==3.8.1 -astroid==3.1.0 -attrs==23.2.0 -autobahn==23.6.2 -Automat==22.10.0 -autopep8==2.1.0 -certifi==2024.2.2 -cffi==1.16.0 +astroid==3.2.4 +autopep8==2.3.1 +certifi==2024.7.4 +cffi==1.17.0 charset-normalizer==3.3.2 -constantly==23.10.4 coreapi==2.3.3 coreschema==0.0.4 -cryptography==42.0.5 -daphne==4.0.0 +cryptography==43.0.0 defusedxml==0.7.1 dill==0.3.8 dj-rest-auth==5.0.2 -Django==3.2.25 +Django==4.2.15 django-allauth==0.58.2 django-filter==23.5 django-rest-swagger==2.2.0 -django-typomatic==2.5.0 +django-sendfile2==0.7.1 +django-typomatic==2.5.2 djangorestframework==3.14.0 drf-writable-nested==0.7.0 -hyperlink==21.0.0 +gunicorn==22.0.0 idna==3.7 -incremental==22.10.0 isort==5.13.2 itypes==1.2.0 -Jinja2==3.1.3 +Jinja2==3.1.4 MarkupSafe==2.1.5 mccabe==0.7.0 oauthlib==3.2.2 openapi-codec==1.3.2 +packaging==24.1 pillow==10.3.0 -platformdirs==4.2.0 -pyasn1==0.6.0 -pyasn1_modules==0.4.0 -pycodestyle==2.11.1 +platformdirs==4.2.2 +psycopg==3.1.20 +pycodestyle==2.12.1 pycparser==2.22 -PyJWT==2.8.0 -pylint==3.1.0 +PyJWT==2.9.0 +pylint==3.2.6 pylint-django==2.5.5 pylint-plugin-utils==0.8.2 -pyOpenSSL==24.1.0 python-magic==0.4.27 python3-openid==3.2.0 pytz==2024.1 -requests==2.31.0 +requests==2.32.3 requests-oauthlib==2.0.0 -service-identity==24.1.0 -simplejson==3.19.2 -six==1.16.0 -sqlparse==0.4.4 -tomlkit==0.12.4 -Twisted==24.3.0 -txaio==23.1.1 -typing_extensions==4.11.0 +simplejson==3.19.3 +sqlparse==0.5.1 +tomlkit==0.13.2 +typing_extensions==4.12.2 Unidecode==1.3.8 uritemplate==4.1.1 -urllib3==2.2.1 -zope.interface==6.3 +urllib3==2.2.2 diff --git a/user/fixtures/users.json b/user/fixtures/users.json index cc67b6df..35498121 100644 --- a/user/fixtures/users.json +++ b/user/fixtures/users.json @@ -121,7 +121,7 @@ "first_name": "Účastník", "last_name": "Strom", "school": "000598071", - "year_of_graduation": 2025 + "year_of_graduation": 2028 } }, { @@ -152,7 +152,7 @@ "first_name": "Účastník", "last_name": "Matik", "school": "000598071", - "year_of_graduation": 2028 + "year_of_graduation": 2031 } }, { @@ -183,7 +183,7 @@ "first_name": "Účastník", "last_name": "Malynár", "school": "000598071", - "year_of_graduation": 2031 + "year_of_graduation": 2034 } }, { @@ -214,7 +214,7 @@ "first_name": "Náhodný", "last_name": "Vysokoškolák", "school": 1, - "year_of_graduation": 2015 + "year_of_graduation": 2024 } }, { diff --git a/user/migrations/0001_initial.py b/user/migrations/0001_initial.py new file mode 100644 index 00000000..34e75912 --- /dev/null +++ b/user/migrations/0001_initial.py @@ -0,0 +1,41 @@ +# Generated by Django 4.2.13 on 2024-06-10 20:10 + +import django.contrib.auth.models +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('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')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('email', models.EmailField(max_length=254, unique=True, verbose_name='email')), + ('verified_email', models.BooleanField(default=False, verbose_name='overený email')), + ('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', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/user/serializers.py b/user/serializers.py index c4d08fe2..2dcd116c 100644 --- a/user/serializers.py +++ b/user/serializers.py @@ -12,7 +12,7 @@ from personal.models import Profile from personal.serializers import ProfileCreateSerializer from user.models import TokenModel -from webstrom.settings import EMAIL_ALERT, EMAIL_NO_REPLY +from webstrom.settings import EMAIL_ALERT def reset_password_url_generator(request, user, temp_key): @@ -172,7 +172,7 @@ def handle_other_school(self, school): 'school_info': school_info }, ), - EMAIL_NO_REPLY, + None, [EMAIL_ALERT] ) diff --git a/webstrom/settings.py b/webstrom/settings.py index 35144f0b..1811890f 100644 --- a/webstrom/settings.py +++ b/webstrom/settings.py @@ -14,6 +14,11 @@ ALLOWED_HOSTS = [] +CSRF_TRUSTED_ORIGINS = [ + 'http://localhost:3000', + 'https://localhost:3000' +] + SITE_ID = 1 # Application definition @@ -45,8 +50,7 @@ 'competition', 'cms', 'user', - 'personal', - 'problem_database' + 'personal' ] INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS @@ -179,8 +183,9 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'media') MEDIA_URL = '/media/' -PRIVATE_STORAGE_ROOT = os.path.join(BASE_DIR, 'protected_media') - +PRIVATE_STORAGE_ROOT = os.path.join(BASE_DIR, 'protected_media/') +SENDFILE_ROOT = PRIVATE_STORAGE_ROOT +SENDFILE_BACKEND = "django_sendfile.backends.simple" # Email backend EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend' @@ -188,7 +193,7 @@ EMAIL_VERIFICATION_TIMEOUT = 86400 -MANAGERS = [('Web', 'web@strom.sk'), ] +MANAGERS = [('Web', 'webmaster@strom.sk'), ] -EMAIL_NO_REPLY = 'no-reply@strom.sk' # z tade sa odosielaju maily +DEFAULT_FROM_EMAIL = 'noreply@strom.sk' # z tade sa odosielaju maily EMAIL_ALERT = 'alert-email-address@strom.sk' # tu sa prijimaju maily diff --git a/webstrom/settings_test.py b/webstrom/settings_test.py new file mode 100644 index 00000000..ff1830be --- /dev/null +++ b/webstrom/settings_test.py @@ -0,0 +1,29 @@ +# pylint: disable=wildcard-import,unused-wildcard-import + +from .settings import * + +DEBUG = True + +ALLOWED_HOSTS = [ + "localhost", + "test.strom.sk", +] + +USE_X_FORWARDED_HOST = True + +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'webstrom-test', + 'USER': 'webstrom', + } +} + +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" + +EMAIL_HOST = "smtp-relay.gmail.com" +EMAIL_PORT = 587 +EMAIL_USE_TLS = True +EMAIL_TIMEOUT = 10 diff --git a/webstrom/urls.py b/webstrom/urls.py index 6b18d490..5a519833 100644 --- a/webstrom/urls.py +++ b/webstrom/urls.py @@ -4,15 +4,15 @@ from django.urls import include, path urlpatterns = [ - path('admin/', admin.site.urls), + path('django-admin/', admin.site.urls), path('user/', include('user.urls')), path('competition/', include('competition.urls')), path('cms/', include('cms.urls')), path('personal/', include('personal.urls')), path('base/', include('base.urls')), + path('protected/', include('downloads.urls')), # Dočasná cesta pre allauth bez rest frameworku path('accounts/', include('allauth.urls')), - path('problem-database/', include('problem_database.urls')) ] # Pri vývoji servuj media files priamo z djanga