From 8eddd0b7967dc889100058b4f382d6200e7fd56a Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 11 Oct 2022 09:45:51 +0300 Subject: [PATCH 01/63] Add djano-stubs --- poetry.lock | 18 +++++++++--------- pyproject.toml | 1 + 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/poetry.lock b/poetry.lock index e9cd52ab..162d384c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -270,7 +270,7 @@ sftp = ["paramiko"] name = "django-stubs" version = "1.12.0" description = "Mypy stubs for Django" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" @@ -290,7 +290,7 @@ compatible-mypy = ["mypy (>=0.930,<0.970)"] name = "django-stubs-ext" version = "0.5.0" description = "Monkey-patching and extensions for django-stubs" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -468,7 +468,7 @@ python-versions = ">=3.6" name = "mypy" version = "0.981" description = "Optional static typing for Python" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" @@ -486,7 +486,7 @@ reports = ["lxml"] name = "mypy-extensions" version = "0.4.3" description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" +category = "main" optional = false python-versions = "*" @@ -836,7 +836,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" @@ -855,7 +855,7 @@ test = ["pre-commit", "pytest"] name = "types-pytz" version = "2022.2.1.0" description = "Typing stubs for pytz" -category = "dev" +category = "main" optional = false python-versions = "*" @@ -863,7 +863,7 @@ python-versions = "*" name = "types-PyYAML" version = "6.0.12" description = "Typing stubs for PyYAML" -category = "dev" +category = "main" optional = false python-versions = "*" @@ -871,7 +871,7 @@ python-versions = "*" name = "typing-extensions" version = "4.3.0" description = "Backported and Experimental Type Hints for Python 3.7+" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" @@ -935,7 +935,7 @@ brotli = ["Brotli"] [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "491a7a2661f59ec87999797bf91b3900e7b0c37584794895e4d51535c4ace9bf" +content-hash = "03693cb3c7e988a1a02e8fa6d5b559858d4beb0a4ce16b2ea5de803edcaff00c" [metadata.files] appnope = [ diff --git a/pyproject.toml b/pyproject.toml index 5f1aa20c..3e2c72e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ django-taggit = "^3.0.0" Pillow = "^9.2.0" requests = "^2.28.1" rich = "^12.5.1" +django-stubs = "^1.12.0" [tool.poetry.dev-dependencies] black = "^22.6.0" From 9d6f23f94e6c0d2d82916ad529631bda6e4fc442 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 11 Oct 2022 09:47:07 +0300 Subject: [PATCH 02/63] Add isort black compatibility --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 3e2c72e7..3c358c0c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,9 @@ django-debug-toolbar = "^3.7.0" django-linear-migrations = "^2.5.1" pre-commit = "^2.20.0" +[tool.isort] +profile = "black" + [tool.mypy] plugins = ["mypy_django_plugin.main"] From 810a1e61606ef1fe54de9e713fb3310c6b85e74e Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 11 Oct 2022 09:50:07 +0300 Subject: [PATCH 03/63] Remove django-stubs from main dependencies --- poetry.lock | 201 +++++++++++++++++++++++++------------------------ pyproject.toml | 3 +- 2 files changed, 102 insertions(+), 102 deletions(-) diff --git a/poetry.lock b/poetry.lock index 162d384c..8fa7b923 100644 --- a/poetry.lock +++ b/poetry.lock @@ -55,11 +55,11 @@ python-versions = "*" [[package]] name = "black" -version = "22.8.0" +version = "22.10.0" description = "The uncompromising code formatter." category = "dev" optional = false -python-versions = ">=3.6.2" +python-versions = ">=3.7" [package.dependencies] click = ">=8.0.0" @@ -270,7 +270,7 @@ sftp = ["paramiko"] name = "django-stubs" version = "1.12.0" description = "Mypy stubs for Django" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" @@ -290,7 +290,7 @@ compatible-mypy = ["mypy (>=0.930,<0.970)"] name = "django-stubs-ext" version = "0.5.0" description = "Monkey-patching and extensions for django-stubs" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" @@ -323,7 +323,7 @@ pytz = "*" [[package]] name = "executing" -version = "1.1.0" +version = "1.1.1" description = "Get the currently executing AST node of a frame, and other information" category = "dev" optional = false @@ -468,7 +468,7 @@ python-versions = ">=3.6" name = "mypy" version = "0.981" description = "Optional static typing for Python" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" @@ -486,7 +486,7 @@ reports = ["lxml"] name = "mypy-extensions" version = "0.4.3" description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "main" +category = "dev" optional = false python-versions = "*" @@ -616,7 +616,7 @@ wcwidth = "*" [[package]] name = "psycopg2-binary" -version = "2.9.3" +version = "2.9.4" description = "psycopg2 - Python-PostgreSQL Database Adapter" category = "main" optional = false @@ -836,7 +836,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" @@ -853,9 +853,9 @@ test = ["pre-commit", "pytest"] [[package]] name = "types-pytz" -version = "2022.2.1.0" +version = "2022.4.0.0" description = "Typing stubs for pytz" -category = "main" +category = "dev" optional = false python-versions = "*" @@ -863,15 +863,15 @@ python-versions = "*" name = "types-PyYAML" version = "6.0.12" description = "Typing stubs for PyYAML" -category = "main" +category = "dev" optional = false python-versions = "*" [[package]] name = "typing-extensions" -version = "4.3.0" +version = "4.4.0" description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" @@ -935,7 +935,7 @@ brotli = ["Brotli"] [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "03693cb3c7e988a1a02e8fa6d5b559858d4beb0a4ce16b2ea5de803edcaff00c" +content-hash = "491a7a2661f59ec87999797bf91b3900e7b0c37584794895e4d51535c4ace9bf" [metadata.files] appnope = [ @@ -959,29 +959,27 @@ backcall = [ {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, ] black = [ - {file = "black-22.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce957f1d6b78a8a231b18e0dd2d94a33d2ba738cd88a7fe64f53f659eea49fdd"}, - {file = "black-22.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5107ea36b2b61917956d018bd25129baf9ad1125e39324a9b18248d362156a27"}, - {file = "black-22.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8166b7bfe5dcb56d325385bd1d1e0f635f24aae14b3ae437102dedc0c186747"}, - {file = "black-22.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd82842bb272297503cbec1a2600b6bfb338dae017186f8f215c8958f8acf869"}, - {file = "black-22.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d839150f61d09e7217f52917259831fe2b689f5c8e5e32611736351b89bb2a90"}, - {file = "black-22.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a05da0430bd5ced89176db098567973be52ce175a55677436a271102d7eaa3fe"}, - {file = "black-22.8.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a098a69a02596e1f2a58a2a1c8d5a05d5a74461af552b371e82f9fa4ada8342"}, - {file = "black-22.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5594efbdc35426e35a7defa1ea1a1cb97c7dbd34c0e49af7fb593a36bd45edab"}, - {file = "black-22.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a983526af1bea1e4cf6768e649990f28ee4f4137266921c2c3cee8116ae42ec3"}, - {file = "black-22.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b2c25f8dea5e8444bdc6788a2f543e1fb01494e144480bc17f806178378005e"}, - {file = "black-22.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:78dd85caaab7c3153054756b9fe8c611efa63d9e7aecfa33e533060cb14b6d16"}, - {file = "black-22.8.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cea1b2542d4e2c02c332e83150e41e3ca80dc0fb8de20df3c5e98e242156222c"}, - {file = "black-22.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b879eb439094751185d1cfdca43023bc6786bd3c60372462b6f051efa6281a5"}, - {file = "black-22.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a12e4e1353819af41df998b02c6742643cfef58282915f781d0e4dd7a200411"}, - {file = "black-22.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3a73f66b6d5ba7288cd5d6dad9b4c9b43f4e8a4b789a94bf5abfb878c663eb3"}, - {file = "black-22.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:e981e20ec152dfb3e77418fb616077937378b322d7b26aa1ff87717fb18b4875"}, - {file = "black-22.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8ce13ffed7e66dda0da3e0b2eb1bdfc83f5812f66e09aca2b0978593ed636b6c"}, - {file = "black-22.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:32a4b17f644fc288c6ee2bafdf5e3b045f4eff84693ac069d87b1a347d861497"}, - {file = "black-22.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ad827325a3a634bae88ae7747db1a395d5ee02cf05d9aa7a9bd77dfb10e940c"}, - {file = "black-22.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53198e28a1fb865e9fe97f88220da2e44df6da82b18833b588b1883b16bb5d41"}, - {file = "black-22.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:bc4d4123830a2d190e9cc42a2e43570f82ace35c3aeb26a512a2102bce5af7ec"}, - {file = "black-22.8.0-py3-none-any.whl", hash = "sha256:d2c21d439b2baf7aa80d6dd4e3659259be64c6f49dfd0f32091063db0e006db4"}, - {file = "black-22.8.0.tar.gz", hash = "sha256:792f7eb540ba9a17e8656538701d3eb1afcb134e3b45b71f20b25c77a8db7e6e"}, + {file = "black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa"}, + {file = "black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef"}, + {file = "black-22.10.0-1fixedarch-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:197df8509263b0b8614e1df1756b1dd41be6738eed2ba9e9769f3880c2b9d7b6"}, + {file = "black-22.10.0-1fixedarch-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:2644b5d63633702bc2c5f3754b1b475378fbbfb481f62319388235d0cd104c2d"}, + {file = "black-22.10.0-1fixedarch-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:e41a86c6c650bcecc6633ee3180d80a025db041a8e2398dcc059b3afa8382cd4"}, + {file = "black-22.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2039230db3c6c639bd84efe3292ec7b06e9214a2992cd9beb293d639c6402edb"}, + {file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"}, + {file = "black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66"}, + {file = "black-22.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b9b29da4f564ba8787c119f37d174f2b69cdfdf9015b7d8c5c16121ddc054ae"}, + {file = "black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b"}, + {file = "black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d"}, + {file = "black-22.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650"}, + {file = "black-22.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d"}, + {file = "black-22.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fba8a281e570adafb79f7755ac8721b6cf1bbf691186a287e990c7929c7692ff"}, + {file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"}, + {file = "black-22.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395"}, + {file = "black-22.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:974308c58d057a651d182208a484ce80a26dac0caef2895836a92dd6ebd725e0"}, + {file = "black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383"}, + {file = "black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de"}, + {file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"}, + {file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"}, ] certifi = [ {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, @@ -1116,8 +1114,8 @@ djangorestframework = [ {file = "djangorestframework-3.14.0.tar.gz", hash = "sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8"}, ] executing = [ - {file = "executing-1.1.0-py2.py3-none-any.whl", hash = "sha256:4a6d96ba89eb3dcc11483471061b42b9006d8c9f81c584dd04246944cd022530"}, - {file = "executing-1.1.0.tar.gz", hash = "sha256:2c2c07d1ec4b2d8f9676b25170f1d8445c0ee2eb78901afb075a4b8d83608c6a"}, + {file = "executing-1.1.1-py2.py3-none-any.whl", hash = "sha256:236ea5f059a38781714a8bfba46a70fad3479c2f552abee3bbafadc57ed111b8"}, + {file = "executing-1.1.1.tar.gz", hash = "sha256:b0d7f8dcc2bac47ce6e39374397e7acecea6fdc380a6d5323e26185d70f38ea8"}, ] filelock = [ {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"}, @@ -1290,62 +1288,65 @@ prompt-toolkit = [ {file = "prompt_toolkit-3.0.31.tar.gz", hash = "sha256:9ada952c9d1787f52ff6d5f3484d0b4df8952787c087edf6a1f7c2cb1ea88148"}, ] psycopg2-binary = [ - {file = "psycopg2-binary-2.9.3.tar.gz", hash = "sha256:761df5313dc15da1502b21453642d7599d26be88bff659382f8f9747c7ebea4e"}, - {file = "psycopg2_binary-2.9.3-cp310-cp310-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:539b28661b71da7c0e428692438efbcd048ca21ea81af618d845e06ebfd29478"}, - {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e82d38390a03da28c7985b394ec3f56873174e2c88130e6966cb1c946508e65"}, - {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57804fc02ca3ce0dbfbef35c4b3a4a774da66d66ea20f4bda601294ad2ea6092"}, - {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:083a55275f09a62b8ca4902dd11f4b33075b743cf0d360419e2051a8a5d5ff76"}, - {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:0a29729145aaaf1ad8bafe663131890e2111f13416b60e460dae0a96af5905c9"}, - {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a79d622f5206d695d7824cbf609a4f5b88ea6d6dab5f7c147fc6d333a8787e4"}, - {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:090f3348c0ab2cceb6dfbe6bf721ef61262ddf518cd6cc6ecc7d334996d64efa"}, - {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a9e1f75f96ea388fbcef36c70640c4efbe4650658f3d6a2967b4cc70e907352e"}, - {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c3ae8e75eb7160851e59adc77b3a19a976e50622e44fd4fd47b8b18208189d42"}, - {file = "psycopg2_binary-2.9.3-cp310-cp310-win32.whl", hash = "sha256:7b1e9b80afca7b7a386ef087db614faebbf8839b7f4db5eb107d0f1a53225029"}, - {file = "psycopg2_binary-2.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:8b344adbb9a862de0c635f4f0425b7958bf5a4b927c8594e6e8d261775796d53"}, - {file = "psycopg2_binary-2.9.3-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:e847774f8ffd5b398a75bc1c18fbb56564cda3d629fe68fd81971fece2d3c67e"}, - {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68641a34023d306be959101b345732360fc2ea4938982309b786f7be1b43a4a1"}, - {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3303f8807f342641851578ee7ed1f3efc9802d00a6f83c101d21c608cb864460"}, - {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:e3699852e22aa68c10de06524a3721ade969abf382da95884e6a10ff798f9281"}, - {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:526ea0378246d9b080148f2d6681229f4b5964543c170dd10bf4faaab6e0d27f"}, - {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:b1c8068513f5b158cf7e29c43a77eb34b407db29aca749d3eb9293ee0d3103ca"}, - {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:15803fa813ea05bef089fa78835118b5434204f3a17cb9f1e5dbfd0b9deea5af"}, - {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:152f09f57417b831418304c7f30d727dc83a12761627bb826951692cc6491e57"}, - {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:404224e5fef3b193f892abdbf8961ce20e0b6642886cfe1fe1923f41aaa75c9d"}, - {file = "psycopg2_binary-2.9.3-cp36-cp36m-win32.whl", hash = "sha256:1f6b813106a3abdf7b03640d36e24669234120c72e91d5cbaeb87c5f7c36c65b"}, - {file = "psycopg2_binary-2.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:2d872e3c9d5d075a2e104540965a1cf898b52274a5923936e5bfddb58c59c7c2"}, - {file = "psycopg2_binary-2.9.3-cp37-cp37m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:10bb90fb4d523a2aa67773d4ff2b833ec00857f5912bafcfd5f5414e45280fb1"}, - {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a52ecab70af13e899f7847b3e074eeb16ebac5615665db33bce8a1009cf33"}, - {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a29b3ca4ec9defec6d42bf5feb36bb5817ba3c0230dd83b4edf4bf02684cd0ae"}, - {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:12b11322ea00ad8db8c46f18b7dfc47ae215e4df55b46c67a94b4effbaec7094"}, - {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:53293533fcbb94c202b7c800a12c873cfe24599656b341f56e71dd2b557be063"}, - {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c381bda330ddf2fccbafab789d83ebc6c53db126e4383e73794c74eedce855ef"}, - {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d29409b625a143649d03d0fd7b57e4b92e0ecad9726ba682244b73be91d2fdb"}, - {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:183a517a3a63503f70f808b58bfbf962f23d73b6dccddae5aa56152ef2bcb232"}, - {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:15c4e4cfa45f5a60599d9cec5f46cd7b1b29d86a6390ec23e8eebaae84e64554"}, - {file = "psycopg2_binary-2.9.3-cp37-cp37m-win32.whl", hash = "sha256:adf20d9a67e0b6393eac162eb81fb10bc9130a80540f4df7e7355c2dd4af9fba"}, - {file = "psycopg2_binary-2.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:2f9ffd643bc7349eeb664eba8864d9e01f057880f510e4681ba40a6532f93c71"}, - {file = "psycopg2_binary-2.9.3-cp38-cp38-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:def68d7c21984b0f8218e8a15d514f714d96904265164f75f8d3a70f9c295667"}, - {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dffc08ca91c9ac09008870c9eb77b00a46b3378719584059c034b8945e26b272"}, - {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:280b0bb5cbfe8039205c7981cceb006156a675362a00fe29b16fbc264e242834"}, - {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:af9813db73395fb1fc211bac696faea4ca9ef53f32dc0cfa27e4e7cf766dcf24"}, - {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:63638d875be8c2784cfc952c9ac34e2b50e43f9f0a0660b65e2a87d656b3116c"}, - {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ffb7a888a047696e7f8240d649b43fb3644f14f0ee229077e7f6b9f9081635bd"}, - {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0c9d5450c566c80c396b7402895c4369a410cab5a82707b11aee1e624da7d004"}, - {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:d1c1b569ecafe3a69380a94e6ae09a4789bbb23666f3d3a08d06bbd2451f5ef1"}, - {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8fc53f9af09426a61db9ba357865c77f26076d48669f2e1bb24d85a22fb52307"}, - {file = "psycopg2_binary-2.9.3-cp38-cp38-win32.whl", hash = "sha256:6472a178e291b59e7f16ab49ec8b4f3bdada0a879c68d3817ff0963e722a82ce"}, - {file = "psycopg2_binary-2.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:35168209c9d51b145e459e05c31a9eaeffa9a6b0fd61689b48e07464ffd1a83e"}, - {file = "psycopg2_binary-2.9.3-cp39-cp39-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:47133f3f872faf28c1e87d4357220e809dfd3fa7c64295a4a148bcd1e6e34ec9"}, - {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91920527dea30175cc02a1099f331aa8c1ba39bf8b7762b7b56cbf54bc5cce42"}, - {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887dd9aac71765ac0d0bac1d0d4b4f2c99d5f5c1382d8b770404f0f3d0ce8a39"}, - {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:1f14c8b0942714eb3c74e1e71700cbbcb415acbc311c730370e70c578a44a25c"}, - {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:7af0dd86ddb2f8af5da57a976d27cd2cd15510518d582b478fbb2292428710b4"}, - {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:93cd1967a18aa0edd4b95b1dfd554cf15af657cb606280996d393dadc88c3c35"}, - {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bda845b664bb6c91446ca9609fc69f7db6c334ec5e4adc87571c34e4f47b7ddb"}, - {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:01310cf4cf26db9aea5158c217caa92d291f0500051a6469ac52166e1a16f5b7"}, - {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:99485cab9ba0fa9b84f1f9e1fef106f44a46ef6afdeec8885e0b88d0772b49e8"}, - {file = "psycopg2_binary-2.9.3-cp39-cp39-win32.whl", hash = "sha256:46f0e0a6b5fa5851bbd9ab1bc805eef362d3a230fbdfbc209f4a236d0a7a990d"}, - {file = "psycopg2_binary-2.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:accfe7e982411da3178ec690baaceaad3c278652998b2c45828aaac66cd8285f"}, + {file = "psycopg2-binary-2.9.4.tar.gz", hash = "sha256:a6a2d3d75d8698dee492f4af7ad07606d0734e581edf9e2ce2f74b6fce90f42e"}, + {file = "psycopg2_binary-2.9.4-cp310-cp310-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:e72491d72870c3cb2f0d6f4174485533caec0e9ed7e717e2859b7cc7ff2ae1c4"}, + {file = "psycopg2_binary-2.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2903bf90b1e6bfc9bbfc94a1db0b50ffa9830a0ca4c042fbc38d93890c02ce08"}, + {file = "psycopg2_binary-2.9.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15e0ac0ed8a85f6049e836e95ddee627766561c85be8d23f4b3edb6ddbaa7310"}, + {file = "psycopg2_binary-2.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:edf0a66ce9517365c7dcfed597894d8dd1f27b59e550b77a089054101435213b"}, + {file = "psycopg2_binary-2.9.4-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:61c6a258469c66412ae8358a0501df6ccb3bb48aa9c43b56624571ff9767f91d"}, + {file = "psycopg2_binary-2.9.4-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:704f1fcdc5b606b70563ea696c69bda90caee3a2f45ffc9cee60a901b394a79f"}, + {file = "psycopg2_binary-2.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:30200b07779446760813eef06098ec6d084131e4365b4e023eb43100de758b11"}, + {file = "psycopg2_binary-2.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f5fbb3b325c65010e04af206a9243e2df8606736c510c7f268aca6a93e5294a9"}, + {file = "psycopg2_binary-2.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:52383e932e6de5595963f9178cf2af7b9e1f3daacf5135b9c0e21aabbc5bf7c4"}, + {file = "psycopg2_binary-2.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0d8e0c9eec79fe1ae66691e06e3cc714da6fbd77981209bf32fa823c03dbaff8"}, + {file = "psycopg2_binary-2.9.4-cp310-cp310-win32.whl", hash = "sha256:161dc52a617f0bb610a87d391cb2e77fe65b89ebfbd752f4f3217dde701ea196"}, + {file = "psycopg2_binary-2.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:33ac8b4754e6b6b21f3ee180da169d8526d91aee9408ec1fc573c16ab32b0207"}, + {file = "psycopg2_binary-2.9.4-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:7751b11cd7f6b952b4b5ec5b93b5be9ce20faba786c18c25c354f5d8717a173c"}, + {file = "psycopg2_binary-2.9.4-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b216a15e13f6e763db40ac3beb74b588650bc030d10a78fde182b88d273b82b5"}, + {file = "psycopg2_binary-2.9.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0eae72190be519bf2629062eab7ac8d4ceec5bd132953cefa1596584d86964fe"}, + {file = "psycopg2_binary-2.9.4-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:fb639a0e65dce4a9cccbcbdd8ddd0c8c6ab10bca317b827a5c52ac3c3a4ad60a"}, + {file = "psycopg2_binary-2.9.4-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:80ed219ce6cb21a5b53ead0edf5b56b6d23de4cb95389ac606f47670474f4816"}, + {file = "psycopg2_binary-2.9.4-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:f78cafa25731e0b5aa16fe20bea1abf643d4e853f6bfb8a64421b06b878e2b88"}, + {file = "psycopg2_binary-2.9.4-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:34fd249275faa782c3a2016e86ac2330636ac58d731a1580e7d686e3976b9536"}, + {file = "psycopg2_binary-2.9.4-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:24d627ed69e754c48dd142a914124858c600b4108c92546eb0ba822e63c0c6e2"}, + {file = "psycopg2_binary-2.9.4-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:65d5f4e70a2d3fbaa1349236968792611088f3f2dccead36c1626e1d183cc327"}, + {file = "psycopg2_binary-2.9.4-cp36-cp36m-win32.whl", hash = "sha256:ae5b41dbf7731b838021923edfbe3b5ccdec84d92d5795f5229c0d08d32509d9"}, + {file = "psycopg2_binary-2.9.4-cp36-cp36m-win_amd64.whl", hash = "sha256:97e4f3d9b17d12e7c00cb1c29c0040044135cd5146838da4274615dbe0baae78"}, + {file = "psycopg2_binary-2.9.4-cp37-cp37m-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:8660112e9127a019969a23c878e1b4a419e8a6427f9a9050c19830f152628c8a"}, + {file = "psycopg2_binary-2.9.4-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea8d5cd689fa7225d81ae0a049ba03e0165f4ed9ca083b19a405be9ad0b36845"}, + {file = "psycopg2_binary-2.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63edc507f8cbfbb5903adb75bad8a99f9798981c854df9119dbebab2ec3ee0e1"}, + {file = "psycopg2_binary-2.9.4-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:25e0517ad7ee3c5c3c69dbe3c1d95504c811e42f452b39a3505d0763b1f6caa0"}, + {file = "psycopg2_binary-2.9.4-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:0a9465f0aa36480c8e7614991cbe8ca8aa16b0517c5398a49648ce345e446c19"}, + {file = "psycopg2_binary-2.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:aff258af03dda9a990960a53759d10c3a9b936837c71fe2f3b581acd356b9121"}, + {file = "psycopg2_binary-2.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:576b9dfbcd154a0e8b5d9dae6316d037450e64a3b31df87dec71d88e2a2d5e5f"}, + {file = "psycopg2_binary-2.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:044b6ab68613de7ea1e63856627deea091bfea09dea5ab4f050b13250fd18cab"}, + {file = "psycopg2_binary-2.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7b47643c45e7619788c081d42e1d9d98c7c8a4933010a9967d097cc3c4c29f41"}, + {file = "psycopg2_binary-2.9.4-cp37-cp37m-win32.whl", hash = "sha256:82df4a8600999c4c0cb7d6614df1bbdb3c74732f63e79f78487893ffbed3d083"}, + {file = "psycopg2_binary-2.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:8d7bc25729bb6d96b44f49ad78fde0e27a1a867cb205322b7e5f5b49e04d6f1f"}, + {file = "psycopg2_binary-2.9.4-cp38-cp38-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:2f1ded23d17af0d738e7e78087f0b88a53228887845b1989b03af4dfd3fef703"}, + {file = "psycopg2_binary-2.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f225784812b2b57d340f2eb0d2cebef989dcc82c288f5553e28ee9767c7c8344"}, + {file = "psycopg2_binary-2.9.4-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89a86c2b35460700d04b4d6461153ab39ee85af5a5385acac9563a8310e6320a"}, + {file = "psycopg2_binary-2.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59a3010d566a48b919490a982f6807f68842686941dc12d568e129d9cd7703d6"}, + {file = "psycopg2_binary-2.9.4-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:02cde837df012fa5d579b9cf4bc8e1feb460f38d61f7a4ab4a919d55a9f6eeef"}, + {file = "psycopg2_binary-2.9.4-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:226f11be577b70a57f4910c0ee28591d4d9fcb3d455e966267179156ae2e0c41"}, + {file = "psycopg2_binary-2.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:181ac372a5a5308b4076933601a9b5f0cd139b389b0aa5e164786a2abbcdb978"}, + {file = "psycopg2_binary-2.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5f27b1d1b56470385faa2b2636fcb823e7ac5b5b734e0aa76b14637c66eb3b7"}, + {file = "psycopg2_binary-2.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:55137faec669c4277c5687c6ce7c1fbc4dece0e2f14256ee808f4a652f0a2170"}, + {file = "psycopg2_binary-2.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ffb2f288f577a748cc23c65a818290755a4c2da1f87a40d7055b61a096d31e20"}, + {file = "psycopg2_binary-2.9.4-cp38-cp38-win32.whl", hash = "sha256:451550e0bb5889bbabbf92575a6d6eafced941cc28c86be6ae4667f81bf32d67"}, + {file = "psycopg2_binary-2.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:b23b25b1243576b952689966205ef7d4285688068b966a1ca0e620bcb390d483"}, + {file = "psycopg2_binary-2.9.4-cp39-cp39-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:7ad9d032dc1a31a86ca7b059f43554a049a2bfda8fe32d1492ad25f6686aff03"}, + {file = "psycopg2_binary-2.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7b01d07006a0ac2216921b69a220b9f0974345d0b1b36efaeabdc7550b1cc4f8"}, + {file = "psycopg2_binary-2.9.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb5341fc7c53fdd95ac2415be77b1de854ab266488cff71174ebb007baf0e675"}, + {file = "psycopg2_binary-2.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a431deb6ffdfa551f7400b3a94fa4b964837e67f49e3c37aa26d90dc75970816"}, + {file = "psycopg2_binary-2.9.4-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:d6ba33f39436191ece7ea2b3d0b4dff00af71acd5c6e6f1d6b7563aa7286e9f2"}, + {file = "psycopg2_binary-2.9.4-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:d6c5e1df6f427d7a82606cf8f07cf3ba9fb3f366804b01e65f1f00f8df6b54f1"}, + {file = "psycopg2_binary-2.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1c22c59ab7d9dc110d409445f111f58556bf699b0548f3fc5176684a29c629c4"}, + {file = "psycopg2_binary-2.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b896637091cde69d170a89253dde9aee814b25ca204b7e213fd0a6462e666638"}, + {file = "psycopg2_binary-2.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:2535f44b00f26f6af0e949c825e6aecb9adcb56c965c17af5b97137fb69f00c0"}, + {file = "psycopg2_binary-2.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6a1618260a112a9c93504511f0b6254b4402a8c41b7130dc6d4c9e39aff3aa0c"}, + {file = "psycopg2_binary-2.9.4-cp39-cp39-win32.whl", hash = "sha256:e02f77b620ad6b36564fe41980865436912e21a3b1138cdde175cf24afde1bc5"}, + {file = "psycopg2_binary-2.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:44f5dc9b4384bafca8429759ce76c8960ffc2b583fcad9e5dfb3e5f4894269e4"}, ] ptyprocess = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, @@ -1470,16 +1471,16 @@ traitlets = [ {file = "traitlets-5.4.0.tar.gz", hash = "sha256:3f2c4e435e271592fe4390f1746ea56836e3a080f84e7833f0f801d9613fec39"}, ] types-pytz = [ - {file = "types-pytz-2022.2.1.0.tar.gz", hash = "sha256:47cfb19c52b9f75896440541db392fd312a35b279c6307a531db71152ea63e2b"}, - {file = "types_pytz-2022.2.1.0-py3-none-any.whl", hash = "sha256:50ead2254b524a3d4153bc65d00289b66898060d2938e586170dce918dbaf3b3"}, + {file = "types-pytz-2022.4.0.0.tar.gz", hash = "sha256:17d66e4b16e80ceae0787726f3a22288df7d3f9fdebeb091dc64b92c0e4ea09d"}, + {file = "types_pytz-2022.4.0.0-py3-none-any.whl", hash = "sha256:950b0f3d64ed5b03a3e29c1e38fe2be8371c933c8e97922d0352345336eb8af4"}, ] types-PyYAML = [ {file = "types-PyYAML-6.0.12.tar.gz", hash = "sha256:f6f350418125872f3f0409d96a62a5a5ceb45231af5cc07ee0034ec48a3c82fa"}, {file = "types_PyYAML-6.0.12-py3-none-any.whl", hash = "sha256:29228db9f82df4f1b7febee06bbfb601677882e98a3da98132e31c6874163e15"}, ] typing-extensions = [ - {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, - {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, + {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, + {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, ] tzdata = [ {file = "tzdata-2022.4-py2.py3-none-any.whl", hash = "sha256:74da81ecf2b3887c94e53fc1d466d4362aaf8b26fc87cda18f22004544694583"}, diff --git a/pyproject.toml b/pyproject.toml index 3c358c0c..5d596751 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,6 @@ django-taggit = "^3.0.0" Pillow = "^9.2.0" requests = "^2.28.1" rich = "^12.5.1" -django-stubs = "^1.12.0" [tool.poetry.dev-dependencies] black = "^22.6.0" @@ -42,7 +41,7 @@ profile = "black" plugins = ["mypy_django_plugin.main"] [tool.django-stubs] -django_settings_module = "myproject.settings" +django_settings_module = "project.core.settings" [build-system] requires = ["poetry-core>=1.0.0"] From f4119c7ec9df1625583644164d56bcdeace19601 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 11 Oct 2022 09:54:29 +0300 Subject: [PATCH 04/63] Add django-stubs to mypy config --- .pre-commit-config.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 51948868..62c58f99 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,8 +39,9 @@ repos: - id: curlylint - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.981 + rev: v0.981 hooks: - id: mypy args: [--no-strict-optional, --ignore-missing-imports] - + additional_dependencies: + - django-stubs From 1e805e83cfef2df603087bf2f8ea197557f3ceb1 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 11 Oct 2022 09:56:07 +0300 Subject: [PATCH 05/63] Add django-extensions to mypy config --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 62c58f99..7a60b03d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,3 +45,4 @@ repos: args: [--no-strict-optional, --ignore-missing-imports] additional_dependencies: - django-stubs + - django-extensions From ddcec746aab2308fd9226d7d860b54c237d3ec6a Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 11 Oct 2022 09:57:15 +0300 Subject: [PATCH 06/63] Remove mypy pre-commit --- .pre-commit-config.yaml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7a60b03d..167d70fd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,12 +37,3 @@ repos: rev: v0.13.1 hooks: - id: curlylint - - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.981 - hooks: - - id: mypy - args: [--no-strict-optional, --ignore-missing-imports] - additional_dependencies: - - django-stubs - - django-extensions From b63bdd27fd71b2fc4dda24fd3be0eca8b1b7da34 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 11 Oct 2022 09:57:21 +0300 Subject: [PATCH 07/63] Disable unused API endpoints (TODO: remove) --- project/accounts/api.py | 216 +++++++++++++++++------------------ project/accounts/urls/api.py | 10 +- 2 files changed, 109 insertions(+), 117 deletions(-) diff --git a/project/accounts/api.py b/project/accounts/api.py index 74103420..4f693033 100644 --- a/project/accounts/api.py +++ b/project/accounts/api.py @@ -1,36 +1,28 @@ +from accounts.forms import UpdateProfileImage +from accounts.models import Profile +from accounts.permissions import IsProfileOwnerOrDuringRegistrationOrReadOnly +from accounts.serializers import ProfileListSerializer, ProfileSerializer +from accounts.utils import get_account +from categories.models import Category +from categories.serializers import CategorySerializer +from core.custom_decorators import require_post_params from django.contrib.auth import get_user_model from django.contrib.auth.decorators import login_required -from django.shortcuts import get_object_or_404 from django.http import ( - JsonResponse, - HttpResponse, - HttpResponseServerError, - HttpResponseForbidden, HttpResponseBadRequest, + HttpResponseForbidden, + HttpResponseServerError, + JsonResponse, ) - -from rest_framework.viewsets import ModelViewSet -from rest_framework.permissions import IsAuthenticated +from django.shortcuts import get_object_or_404 +from notifications.signals import notify from rest_framework.decorators import action, api_view +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response - -from notifications.signals import notify - -from accounts.permissions import IsProfileOwnerOrDuringRegistrationOrReadOnly -from accounts.serializers import ( - ProfileSerializer, - ProfileListSerializer, - UserSerializer, -) -from accounts.forms import UpdateProfileImage -from accounts.utils import get_account -from threads.models import Thread, Civi, Activity -from accounts.models import Profile -from categories.models import Category +from rest_framework.viewsets import ModelViewSet +from threads.models import Activity, Civi, Thread +from threads.serializers import CiviSerializer, ThreadSerializer from threads.utils import json_response -from threads.serializers import ThreadSerializer, CiviSerializer -from categories.serializers import CategorySerializer -from core.custom_decorators import require_post_params class ProfileViewSet(ModelViewSet): @@ -147,20 +139,20 @@ def get_permissions(self): return super(ProfileViewSet, self).get_permissions() -@api_view(["GET"]) -def get_user(request, username): - """ - USAGE: - This is used to get a user - """ +# @api_view(["GET"]) +# def get_user(request, username): +# """ +# USAGE: +# This is used to get a user +# """ - try: - user = get_user_model().objects.get(username=username) - return JsonResponse(UserSerializer(user).data) - except get_user_model().DoesNotExist: - return JsonResponse( - {"error": f"User with username {username} not found"}, status=400 - ) +# try: +# user = get_user_model().objects.get(username=username) +# return JsonResponse(UserSerializer(user).data) +# except get_user_model().DoesNotExist: +# return JsonResponse( +# {"error": f"User with username {username} not found"}, status=400 +# ) @api_view(["GET"]) @@ -221,26 +213,26 @@ def get_profile(request, username): ) -@api_view(["GET"]) -def get_card(request, username): - """ - USAGE: - This is used to get a card - """ - - try: - user = get_user_model().objects.get(username=username) - profile = user.profile - result = Profile.objects.card_summarize( - profile, Profile.objects.get(user=request.user) - ) - return JsonResponse(result) - except get_user_model().DoesNotExist: - return JsonResponse( - {"error": f"User with username {username} not found"}, status=400 - ) - except Exception as e: - return HttpResponseBadRequest(reason=str(e)) +# @api_view(["GET"]) +# def get_card(request, username): +# """ +# USAGE: +# This is used to get a card +# """ + +# try: +# user = get_user_model().objects.get(username=username) +# profile = user.profile +# result = Profile.objects.card_summarize( +# profile, Profile.objects.get(user=request.user) +# ) +# return JsonResponse(result) +# except get_user_model().DoesNotExist: +# return JsonResponse( +# {"error": f"User with username {username} not found"}, status=400 +# ) +# except Exception as e: +# return HttpResponseBadRequest(reason=str(e)) def get_feed(request): @@ -326,27 +318,27 @@ def upload_profile_image(request): return HttpResponseForbidden("allowed only via POST") -@login_required -def clear_profile_image(request): - """This function is used to delete a profile image""" +# @login_required +# def clear_profile_image(request): +# """This function is used to delete a profile image""" - if request.method == "POST": - try: - account = Profile.objects.get(user=request.user) +# if request.method == "POST": +# try: +# account = Profile.objects.get(user=request.user) - # Clean up previous image - account.profile_image.delete() - account.save() +# # Clean up previous image +# account.profile_image.delete() +# account.save() - return HttpResponse("Image Deleted") - except get_user_model().DoesNotExist: - return HttpResponseServerError( - reason=f"Profile with id:{request.user.username} does not exist" - ) - except Exception: - return HttpResponseServerError(reason=str("default")) - else: - return HttpResponseForbidden("allowed only via POST") +# return HttpResponse("Image Deleted") +# except get_user_model().DoesNotExist: +# return HttpResponseServerError( +# reason=f"Profile with id:{request.user.username} does not exist" +# ) +# except Exception: +# return HttpResponseServerError(reason=str("default")) +# else: +# return HttpResponseForbidden("allowed only via POST") @login_required @@ -463,39 +455,39 @@ def edit_user_categories(request): return HttpResponseServerError(reason=str(e)) -@login_required -def delete_user(request): - """ - Delete User Information - """ - try: - # Get current user - user = get_user_model().objects.get(username=request.user.username) - profile = Profile.objects.get(user=user) - # Expunge personally identifiable data in user obj, feel free to change - data = { - "is_active": False, - "email": "", - "first_name": "", - "last_name": "", - "username": "[Deleted-" + str(user.id) + "]", - } - user.__dict__.update(data) - user.save() # Update into database - - data = { # Expunge personally identifiable data in profile obj - "first_name": "", - "last_name": "", - "about_me": "", - } - profile.__dict__.update(data) - profile.save() - except get_user_model().DoesNotExist as e: - return HttpResponseBadRequest(reason=str(e)) - except Exception as e: - return HttpResponseServerError(reason=str(e)) - - user.refresh_from_db() # Make update viewable - profile.refresh_from_db() - - return JsonResponse({"result": "User successfully deleted."}) +# @login_required +# def delete_user(request): +# """ +# Delete User Information +# """ +# try: +# # Get current user +# user = get_user_model().objects.get(username=request.user.username) +# profile = Profile.objects.get(user=user) +# # Expunge personally identifiable data in user obj, feel free to change +# data = { +# "is_active": False, +# "email": "", +# "first_name": "", +# "last_name": "", +# "username": "[Deleted-" + str(user.id) + "]", +# } +# user.__dict__.update(data) +# user.save() # Update into database + +# data = { # Expunge personally identifiable data in profile obj +# "first_name": "", +# "last_name": "", +# "about_me": "", +# } +# profile.__dict__.update(data) +# profile.save() +# except get_user_model().DoesNotExist as e: +# return HttpResponseBadRequest(reason=str(e)) +# except Exception as e: +# return HttpResponseServerError(reason=str(e)) + +# user.refresh_from_db() # Make update viewable +# profile.refresh_from_db() + +# return JsonResponse({"result": "User successfully deleted."}) diff --git a/project/accounts/urls/api.py b/project/accounts/urls/api.py index 7370fc7b..fdd11958 100644 --- a/project/accounts/urls/api.py +++ b/project/accounts/urls/api.py @@ -1,15 +1,15 @@ -from django.urls import path from accounts import api +from django.urls import path urlpatterns = [ - path("account_data//", api.get_user, name="get_user"), + # path("account_data//", api.get_user, name="get_user"), path("account_profile//", api.get_profile, name="get_profile"), - path("account_card//", api.get_card, name="get_card"), + # path("account_card//", api.get_card, name="get_card"), path("feed/", api.get_feed, name="get_feed"), path("edituser/", api.edit_user, name="edit_user"), - path("deleteuser/", api.delete_user, name="delete_user"), + # path("deleteuser/", api.delete_user, name="delete_user"), path("upload_profile/", api.upload_profile_image, name="upload_profile"), - path("clear_profile/", api.clear_profile_image, name="clear_profile"), + # path("clear_profile/", api.clear_profile_image, name="clear_profile"), path("follow/", api.request_follow, name="follow_user"), path("unfollow/", api.request_unfollow, name="unfollow_user"), path( From f13630e0927d23ce7e97ad67e0e80e44df81529f Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 11 Oct 2022 10:29:28 +0300 Subject: [PATCH 08/63] Simplify UserProfileView --- .../accounts/templates/accounts/account.html | 84 ++++--------------- project/accounts/views.py | 44 +++------- 2 files changed, 28 insertions(+), 100 deletions(-) diff --git a/project/accounts/templates/accounts/account.html b/project/accounts/templates/accounts/account.html index 92dd1d41..a6babb80 100644 --- a/project/accounts/templates/accounts/account.html +++ b/project/accounts/templates/accounts/account.html @@ -79,7 +79,7 @@
{{ user.profile.first_name }} {{ user.profile.last_name }}
-
@{{ username }}
+
@{{ user.username }}
@@ -91,67 +91,15 @@ {% if request.user.username != username|stringformat:"s" %} {% endif %}
-
- -
+
@@ -321,9 +269,9 @@ + data-username="{{ follower.user.username }}"> + Follow +
{% else %}
@@ -416,8 +364,9 @@ class="waves-effect waves-light btn follow-btn col s12" data-userid="{{ following.id }}" data-username="{{ following.user.username }}" - >Follow + > + Follow +
{% else %}
@@ -466,17 +415,16 @@ launchGo to Thread + >launchGo to Thread +
diff --git a/project/accounts/views.py b/project/accounts/views.py index 69dffbee..d560c491 100644 --- a/project/accounts/views.py +++ b/project/accounts/views.py @@ -4,25 +4,23 @@ This module will include views for the accounts app. """ -from core.custom_decorators import full_profile +from accounts.authentication import account_activation_token, send_activation_email +from accounts.forms import ProfileEditForm, UserRegistrationForm +from accounts.models import Profile from django.conf import settings from django.contrib.auth import get_user_model, login from django.contrib.auth import views as auth_views from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.sites.shortcuts import get_current_site from django.http import HttpResponseRedirect +from django.shortcuts import get_object_or_404 from django.template.response import TemplateResponse from django.urls import reverse_lazy from django.utils.encoding import force_str from django.utils.http import urlsafe_base64_decode -from django.utils.decorators import method_decorator from django.views import View from django.views.generic.edit import FormView, UpdateView -from accounts.authentication import account_activation_token, send_activation_email -from accounts.forms import ProfileEditForm, UpdateProfileImage, UserRegistrationForm -from accounts.models import Profile - class RegisterView(FormView): """ @@ -169,32 +167,14 @@ def get(self, request): class UserProfileView(LoginRequiredMixin, View): """A view that shows profile for authorized users""" - @method_decorator(full_profile) def get(self, request, username=None): - if not username: - return HttpResponseRedirect(f"/profile/{request.user}") - else: - is_owner = username == request.user.username - try: - user = get_user_model().objects.get(username=username) - - except get_user_model().DoesNotExist: - return HttpResponseRedirect("/404") - - form = ProfileEditForm( - initial={ - "username": user.username, - "email": user.email, - "first_name": user.profile.first_name or None, - "last_name": user.profile.last_name or None, - "about_me": user.profile.about_me or None, + user_model = get_user_model() + user = get_object_or_404(user_model, username=username) + + return TemplateResponse( + request, + "account.html", + { + "user": user, }, - readonly=True, ) - data = { - "username": user, - "profile_image_form": UpdateProfileImage, - "form": form if is_owner else None, - "readonly": True, - } - return TemplateResponse(request, "account.html", data) From fef456fb35d0cf28c561dd4023888dcc8c236e74 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 11 Oct 2022 10:47:41 +0300 Subject: [PATCH 09/63] Simplify profile page (tabs split to new task) --- .../accounts/templates/accounts/account.html | 443 +----------------- .../accounts/partials/user_civis_list.html | 80 ++++ .../partials/user_followers_list.html | 85 ++++ .../partials/user_following_list.html | 83 ++++ .../accounts/partials/user_threads_list.html | 47 ++ project/core/templates/static/js/account.js | 260 ---------- .../static/js/account_tabs/followers.js | 0 .../static/js/account_tabs/following.js | 0 .../static/js/account_tabs/my_civis.js | 79 ---- .../static/js/account_tabs/my_issues.js | 1 - .../static/js/account_tabs/my_reps.js | 1 - 11 files changed, 302 insertions(+), 777 deletions(-) create mode 100644 project/accounts/templates/accounts/partials/user_civis_list.html create mode 100644 project/accounts/templates/accounts/partials/user_followers_list.html create mode 100644 project/accounts/templates/accounts/partials/user_following_list.html create mode 100644 project/accounts/templates/accounts/partials/user_threads_list.html delete mode 100644 project/core/templates/static/js/account.js delete mode 100644 project/core/templates/static/js/account_tabs/followers.js delete mode 100644 project/core/templates/static/js/account_tabs/following.js delete mode 100644 project/core/templates/static/js/account_tabs/my_civis.js delete mode 100644 project/core/templates/static/js/account_tabs/my_issues.js delete mode 100644 project/core/templates/static/js/account_tabs/my_reps.js diff --git a/project/accounts/templates/accounts/account.html b/project/accounts/templates/accounts/account.html index a6babb80..061cc6ef 100644 --- a/project/accounts/templates/accounts/account.html +++ b/project/accounts/templates/accounts/account.html @@ -11,61 +11,6 @@ {% block content %} {% include "global_nav.html" %}
- -
- -
-
- - -
-
-
-
{% translate "My Civi Activity" %}
-
- - -
-
- -
- {% for civi in user.civis.all %} -
- -
-
{{ civi.body }}
-
- -
- {% empty %} -
-
-
-
No activity
-
-
-
- {% endfor %}
-
-
-
-
{% translate "Followers" %}
-
- - -
-
- -
-

- {{ user.profile.followers.count }} Follower{{ user.profile.followers.count|pluralize }} -

- {% for follower in user.profile.followers.all %} -
-
- -
-
- -
-
- {{ follower.about_me }} -
-
- {% if follower not in request.user.profile.followers.all %} - - {% else %} -
- -
- {% endif %} -
-
-
-
- {% empty %} -
-
-
-
No followers
-
-
-
- {% endfor %} -
-
-
-
-
-
-
{% translate "Following" %}
-
- - -
-
- -
-

{{ user.profile.following.count }} Following

- {% for following in user.profile.following.all %} -
-
- -
-
- -
-
- {{ following.about_me }} -
-
- {% if following not in request.user.profile.followers.all %} - - {% else %} -
- -
- {% endif %} -
-
-
-
- {% empty %} -
-
-
-
- Not following any users -
-
-
-
- {% endfor %} -
-
-
-
-
-
- {% translate "Issues I care about" %} -
-
-
- {% for issue in user.thread %} -
-
-
- {{ issue.category }} - {{ issue.solutions.count }} Solutions -
-
{{ issue.title }}
- - -
- -
-
-
-
- {% for solution in issue.solutions %} -
-
-
{{ solution.user_vote }}
-
{{ solution.title }}
-
{{ solution.body }}
-
-
- {% endfor %} -
- {% empty %} -
-
-
-
No issues found
-
-
-
- {% endfor %} -
-
{% endblock content %} - -{% block extra_js %} - - -{% endblock extra_js %} diff --git a/project/accounts/templates/accounts/partials/user_civis_list.html b/project/accounts/templates/accounts/partials/user_civis_list.html new file mode 100644 index 00000000..01d8875f --- /dev/null +++ b/project/accounts/templates/accounts/partials/user_civis_list.html @@ -0,0 +1,80 @@ +{% for civi in user.civis.all %} +
+ +
+
{{ civi.body }}
+
+ +
+ {% empty %} +
+
+
+
No activity
+
+
+
+{% endfor %} diff --git a/project/accounts/templates/accounts/partials/user_followers_list.html b/project/accounts/templates/accounts/partials/user_followers_list.html new file mode 100644 index 00000000..dbb02e53 --- /dev/null +++ b/project/accounts/templates/accounts/partials/user_followers_list.html @@ -0,0 +1,85 @@ +

+ {{ user.profile.followers.count }} Follower{{ user.profile.followers.count|pluralize }} +

+{% for follower in user.profile.followers.all %} +
+
+ +
+
+ +
+
+ {{ follower.about_me }} +
+
+ {% if follower not in request.user.profile.followers.all %} + + {% else %} +
+ +
+ {% endif %} +
+
+
+
+ {% empty %} +
+
+
+
No followers
+
+
+
+{% endfor %} diff --git a/project/accounts/templates/accounts/partials/user_following_list.html b/project/accounts/templates/accounts/partials/user_following_list.html new file mode 100644 index 00000000..e25a749d --- /dev/null +++ b/project/accounts/templates/accounts/partials/user_following_list.html @@ -0,0 +1,83 @@ +

{{ user.profile.following.count }} Following

+{% for following in user.profile.following.all %} +
+
+ +
+
+ +
+
+ {{ following.about_me }} +
+
+ {% if following not in request.user.profile.followers.all %} + + {% else %} +
+ +
+ {% endif %} +
+
+
+
+ {% empty %} +
+
+
+
+ Not following any users +
+
+
+
+{% endfor %} diff --git a/project/accounts/templates/accounts/partials/user_threads_list.html b/project/accounts/templates/accounts/partials/user_threads_list.html new file mode 100644 index 00000000..f9c17f8f --- /dev/null +++ b/project/accounts/templates/accounts/partials/user_threads_list.html @@ -0,0 +1,47 @@ +{% for issue in user.thread %} +
+
+
+ {{ issue.category }} + {{ issue.solutions.count }} Solutions +
+
{{ issue.title }}
+ + +
+ +
+
+
+
+ {% for solution in issue.solutions %} +
+
+
{{ solution.user_vote }}
+
{{ solution.title }}
+
{{ solution.body }}
+
+
+ {% endfor %} +
+ {% empty %} +
+
+
+
No issues found
+
+
+
+{% endfor %} \ No newline at end of file diff --git a/project/core/templates/static/js/account.js b/project/core/templates/static/js/account.js deleted file mode 100644 index 45da8f13..00000000 --- a/project/core/templates/static/js/account.js +++ /dev/null @@ -1,260 +0,0 @@ -cw = cw || {}; - -cw.AccountModel = BB.Model.extend({ - defaults: function() { - return { - profile_image: "", - username: "", - first_name: "", - last_name: "", - about_me: "", - location: "", - history: [], - followers: [], - following: [], - issues: [], - }; - }, - url: function () { - if (! this.user ) { - throw new Error("This is a race condition! and why we can't have nice things :("); - } - return '/api/account_profile/' + this.user + '/'; - }, - - initialize: function (model, options) { - options = options || {}; - this.user = options.user; - } -}); - - -// And hereon commences a pile of horrendous code. Beware! -// TODO: review rewrite refactor. (please) -cw.AccountView = BB.View.extend({ - el: '#account', - template: _.template($('#account-template').html()), - settingsTemplate: _.template($('#settings-template').html()), - sidebarTemplate: _.template($('#sidebar-template').html()), - - // Account Tabs Templates - mycivisTemplate: _.template($('#my-civis-template').html()), - followersTemplate: _.template($('#followers-template').html()), - followingTemplate: _.template($('#following-template').html()), - myissuesTemplate: _.template($('#my-issues-template').html()), - - // Partials - userCardTemplate: _.template($('.user-card-template').html()), - - - initialize: function (options) { - options = options || {}; - this.current_user = options.current_user; - this.isSave = false; - - this.listenTo(this.model, 'sync', function(){ - document.title = this.model.get("first_name") +" "+ this.model.get("last_name") +" (@"+this.model.get("username")+")"; - - this.postRender(); - - if (_.find(this.model.get("followers"), function(follower){ - return (follower.username== current_user); - })) { - var follow_btn = this.$('#sidebar-follow-btn'); - follow_btn.addClass("btn-secondary"); - follow_btn.data("follow-state", true); - follow_btn.html(""); - } - }); - - return this; - }, - - render: function () { - if (this.isSave) { - this.postRender(); - } else { - this.$el.empty().append(this.template()); - - $('.account-tabs .tab').on('dragstart', function() {return false;}); - this.$el.find('.account-settings').pushpin({ top: $('.account-settings').offset().top }); - this.$el.find('.scroll-col').height($(window).height()); - } - }, - - tabsRender: function () { - this.$('#civis').empty().append(this.mycivisTemplate()); - this.$('#followers').empty().append(this.followersTemplate()); - this.$('#following').empty().append(this.followingTemplate()); - this.$('#issues').empty().append(this.myissuesTemplate()); - var settingsEl = this.$('#settings'); - if (settingsEl.length) { - settingsEl.empty().append(this.settingsTemplate()); - Materialize.updateTextFields(); - } - }, - - postRender: function () { - // Timestamp the image with a cachebreaker so that proper refersh occurs - this.model.set({"profile_image": this.model.get("profile_image") + "?" + new Date().getTime() }); - this.$el.find('.account-settings').empty().append(this.sidebarTemplate()); - this.tabsRender(); - cw.materializeShit(); - this.isSave = false; - - }, - - events: { - 'click .follow-btn': 'followRequest', - 'submit #profile_image_form': 'handleFiles', - 'blur .save-account': 'saveAccount', - 'click .toggle-solutions': 'toggleSolutions', - 'change .profile-image-pick': 'previewImage', - 'keypress .save-account': cw.checkForEnter, - }, - - toggleSolutions: function(e) { - var id = $(e.currentTarget).data('id'); - var textElement = $(e.currentTarget).find('.button-text'); - var new_text = textElement.text() === "Show Solutions" ? "Hide Solutions" : "Show Solutions"; - textElement.text(new_text); - this.$('#solutions-'+id).toggleClass('hide'); - }, - - previewImage: function(e){ - var _this = this; - var img = this.$el.find('#id_profile_image'); - if (img.val()) { - var uploaded_image = img[0].files[0]; - if (uploaded_image) { - var formData = new FormData(this.$el.find('#profile_image_form')[0]); - - var reader = new FileReader(); - - reader.onload = function(e) { - var preview_image = _this.$el.find('.preview-image'); - preview_image.attr('src', e.target.result); - - _this.toggleImgButtons(); - }; - reader.readAsDataURL(uploaded_image); - } - } - }, - - showRawRatings: function(e) { - $(e.target).closest('.rating').find('.rating-score').toggleClass('hide'); - $(e.target).closest('.rating').find('.rating-percent').toggleClass('hide'); - }, - - toggleImgButtons: function(event) { - this.$('.profile-image-pick').toggleClass('hide'); - this.$('.upload-image').toggleClass('hide'); - this.$('#confirmation-prompt').toggleClass('hide'); - }, - - followRequest: function(e){ - var apiData = {}, - _this = this, - follow_state = this.$(e.currentTarget).data("follow-state"); - target = this.$(e.target); - - apiData.target = this.$(e.currentTarget).data("username"); - - if (!follow_state) { - $.ajax({ - url: '/api/follow/', - type: 'POST', - data: apiData, - success: function () { - Materialize.toast('You are now following user '+ apiData.target, 5000); - target.addClass("btn-secondary"); - target.data("follow-state", true); - target.html(""); - - }, - error: function () { - Materialize.toast('Could not follow user '+ apiData.target, 5000); - } - }); - } else { - $.ajax({ - url: '/api/unfollow/', - type: 'POST', - data: apiData, - success: function () { - Materialize.toast('You have unfollowed user '+ apiData.target, 5000); - target.removeClass("btn-secondary"); - target.html("FOLLOW"); - target.data("follow-state", false); - }, - error: function () { - Materialize.toast('Could not unfollow user '+ apiData.target, 5000); - } - }); - } - - }, - - saveAccount: function (e) { - var $this = $(e.target), - changeKey = $this.attr('id'), - changeVal = $this.val().trim(), - apiData = {}, - _this = this; - - if (this.model.get([changeKey]) === changeVal) { - return; - } - - apiData[changeKey] = changeVal; - - $.ajax({ - url: '/api/edituser/', - type: 'POST', - data: apiData, - success: function () { - Materialize.toast('Saved!', 5000); - - _this.isSave = true; - _this.model.fetch(); - - } - }); - }, - - handleFiles: function(e) { - e.preventDefault(); - - var _this = this, - formData = new FormData($('#profile_image_form')[0]); - - $.ajax({ - url: '/api/upload_profile/', - type: 'POST', - data: formData, - cache: false, - contentType: false, - processData: false, - success: function () { - Materialize.toast('Saved!', 5000); - - _this.isSave = true; - _this.model.fetch(); - }, - error: function(data){ - if (data.status === 400) { - Materialize.toast(data.responseJSON.message, 5000, 'red'); - } else if (data.status === 500) { - Materialize.toast('Internal Server Error', 5000, 'red'); - } else { - Materialize.toast(data.statusText, 5000, 'red'); - } - }, - - }); - this.toggleImgButtons(); - - return false; - }, -}); diff --git a/project/core/templates/static/js/account_tabs/followers.js b/project/core/templates/static/js/account_tabs/followers.js deleted file mode 100644 index e69de29b..00000000 diff --git a/project/core/templates/static/js/account_tabs/following.js b/project/core/templates/static/js/account_tabs/following.js deleted file mode 100644 index e69de29b..00000000 diff --git a/project/core/templates/static/js/account_tabs/my_civis.js b/project/core/templates/static/js/account_tabs/my_civis.js deleted file mode 100644 index 7d85c074..00000000 --- a/project/core/templates/static/js/account_tabs/my_civis.js +++ /dev/null @@ -1,79 +0,0 @@ -cw = cw || {}; - -cw.ProfileCiviModel = BB.Model.extend({ - defaults: function() { - return { - type: "No Type", - title: "Civi Title", - body: "Civi Body", - author: { "username": "None", "profile_image": "/media/profile/happy.png"}, - ratings: {"ratings": [], "total": 0, "names": ['Strongly Disagree', 'Disagree', 'Neutral', 'Agree', 'Strongly Agree']}, - attachments: [], - created: "No Date" - }; - }, - - initialize: function (model, options) { - options = options || {}; - // this.set({ - // ratings: r, - // author: a, - // attachments: a - // }); - } - - -}); - -cw.ProfileCiviCollection = BB.Collection.extend({ - - model: cw.ProfileCiviModel, - - // url: function () { - // if (! this.username ) { - // throw new Error("This is a race condition! and why we can't have nice things :("); - // } - // return '/api/getUserCivis/' + this.user_id + '/'; - // }, - - initialize: function (models, options) { - options = options || {}; - this.listenTo(this.model, 'add', function () { - console.log('a civi was added'); - }); - } -}); - -cw.MyCivisView = BB.View.extend({ - - el: '#civis', - mycivisTemplate: _.template($('#my-civis-template').html()), - - initialize: function (options) { - options = options || {}; - - console.log(options.civis); - this.civis = new cw.ProfileCiviCollection({ - models: options.civis - }); - console.log(this.civis); - this.render(); - }, - - render: function () { - this.$el.empty().append(this.mycivisTemplate()); - }, - - events: { - 'hover .gitem': 'permalink' - }, - - displayRawRating: function (event) { - - }, - - permalink: function(event) { - var _this = this; - }, - -}); diff --git a/project/core/templates/static/js/account_tabs/my_issues.js b/project/core/templates/static/js/account_tabs/my_issues.js deleted file mode 100644 index 76abb76d..00000000 --- a/project/core/templates/static/js/account_tabs/my_issues.js +++ /dev/null @@ -1 +0,0 @@ -// TODO: issues (1. move from general account to here 2. consider merging with civis and creating a summary view) diff --git a/project/core/templates/static/js/account_tabs/my_reps.js b/project/core/templates/static/js/account_tabs/my_reps.js deleted file mode 100644 index 11aac5cc..00000000 --- a/project/core/templates/static/js/account_tabs/my_reps.js +++ /dev/null @@ -1 +0,0 @@ -// TODO: move rep stuff into here From 40e198659e06ff61dd38e69379915d0374018e75 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 11 Oct 2022 12:56:58 +0300 Subject: [PATCH 10/63] Restore delete_user view/endpoint --- project/accounts/api.py | 68 ++++++++++++++++++------------------ project/accounts/urls/api.py | 2 +- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/project/accounts/api.py b/project/accounts/api.py index 4f693033..66fb1192 100644 --- a/project/accounts/api.py +++ b/project/accounts/api.py @@ -455,39 +455,39 @@ def edit_user_categories(request): return HttpResponseServerError(reason=str(e)) -# @login_required -# def delete_user(request): -# """ -# Delete User Information -# """ -# try: -# # Get current user -# user = get_user_model().objects.get(username=request.user.username) -# profile = Profile.objects.get(user=user) -# # Expunge personally identifiable data in user obj, feel free to change -# data = { -# "is_active": False, -# "email": "", -# "first_name": "", -# "last_name": "", -# "username": "[Deleted-" + str(user.id) + "]", -# } -# user.__dict__.update(data) -# user.save() # Update into database - -# data = { # Expunge personally identifiable data in profile obj -# "first_name": "", -# "last_name": "", -# "about_me": "", -# } -# profile.__dict__.update(data) -# profile.save() -# except get_user_model().DoesNotExist as e: -# return HttpResponseBadRequest(reason=str(e)) -# except Exception as e: -# return HttpResponseServerError(reason=str(e)) +@login_required +def delete_user(request): + """ + Delete User Information + """ + try: + # Get current user + user = get_user_model().objects.get(username=request.user.username) + profile = Profile.objects.get(user=user) + # Expunge personally identifiable data in user obj, feel free to change + data = { + "is_active": False, + "email": "", + "first_name": "", + "last_name": "", + "username": "[Deleted-" + str(user.id) + "]", + } + user.__dict__.update(data) + user.save() # Update into database + + data = { # Expunge personally identifiable data in profile obj + "first_name": "", + "last_name": "", + "about_me": "", + } + profile.__dict__.update(data) + profile.save() + except get_user_model().DoesNotExist as e: + return HttpResponseBadRequest(reason=str(e)) + except Exception as e: + return HttpResponseServerError(reason=str(e)) -# user.refresh_from_db() # Make update viewable -# profile.refresh_from_db() + user.refresh_from_db() # Make update viewable + profile.refresh_from_db() -# return JsonResponse({"result": "User successfully deleted."}) + return JsonResponse({"result": "User successfully deleted."}) diff --git a/project/accounts/urls/api.py b/project/accounts/urls/api.py index fdd11958..dae92f2c 100644 --- a/project/accounts/urls/api.py +++ b/project/accounts/urls/api.py @@ -7,7 +7,7 @@ # path("account_card//", api.get_card, name="get_card"), path("feed/", api.get_feed, name="get_feed"), path("edituser/", api.edit_user, name="edit_user"), - # path("deleteuser/", api.delete_user, name="delete_user"), + path("deleteuser/", api.delete_user, name="delete_user"), path("upload_profile/", api.upload_profile_image, name="upload_profile"), # path("clear_profile/", api.clear_profile_image, name="clear_profile"), path("follow/", api.request_follow, name="follow_user"), From 183ba76bc5294c91a1cb5e668b002325d15a3010 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 11 Oct 2022 13:28:05 +0300 Subject: [PATCH 11/63] Move global nav to base.html --- project/accounts/templates/accounts/update_settings.html | 7 +++---- project/core/templates/base.html | 2 ++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/project/accounts/templates/accounts/update_settings.html b/project/accounts/templates/accounts/update_settings.html index 28dae047..13271cad 100644 --- a/project/accounts/templates/accounts/update_settings.html +++ b/project/accounts/templates/accounts/update_settings.html @@ -9,7 +9,6 @@ {% endblock extra_css %} {% block content %} -{% include "global_nav.html" %}
@@ -56,7 +55,7 @@
{%if not readonly %} - + {% endif%}
@@ -70,7 +69,7 @@

@@ -80,6 +79,6 @@
- + {% endblock content %} diff --git a/project/core/templates/base.html b/project/core/templates/base.html index bedcb34c..6f031dd6 100644 --- a/project/core/templates/base.html +++ b/project/core/templates/base.html @@ -36,6 +36,8 @@ {% block backbone_template %}{% endblock backbone_template %} + {% include "global_nav.html" %} + {% block content %}{% endblock content %} {% include "static_footer.html" %} From 6215e40eaa157629f8f031c12026acd760548dcc Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 11 Oct 2022 13:36:24 +0300 Subject: [PATCH 12/63] Remove global nav import --- project/accounts/templates/accounts/account.html | 1 - project/accounts/templates/accounts/feed.html | 1 - project/threads/templates/threads/thread.html | 1 - 3 files changed, 3 deletions(-) diff --git a/project/accounts/templates/accounts/account.html b/project/accounts/templates/accounts/account.html index 061cc6ef..da8a9368 100644 --- a/project/accounts/templates/accounts/account.html +++ b/project/accounts/templates/accounts/account.html @@ -9,7 +9,6 @@ {% endblock extra_css %} {% block content %} -{% include "global_nav.html" %}
- -
From a069468104af196aca3a36d549b1f6d256eb5bd5 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 11 Oct 2022 13:47:10 +0300 Subject: [PATCH 15/63] Add button text --- project/accounts/templates/accounts/update_settings.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/accounts/templates/accounts/update_settings.html b/project/accounts/templates/accounts/update_settings.html index 13271cad..a0e4a021 100644 --- a/project/accounts/templates/accounts/update_settings.html +++ b/project/accounts/templates/accounts/update_settings.html @@ -55,7 +55,7 @@
{%if not readonly %} - + {% endif%}
From 94a90133d988353b2bd8adb9db36fb8aba88da95 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 11 Oct 2022 13:49:57 +0300 Subject: [PATCH 16/63] Use only Django for Settings view --- .../accounts/templates/accounts/settings.html | 137 +++++++++--------- .../templates/accounts/update_settings.html | 84 ----------- project/accounts/views.py | 2 +- project/core/templates/static/js/settings.js | 84 ----------- 4 files changed, 71 insertions(+), 236 deletions(-) delete mode 100644 project/accounts/templates/accounts/update_settings.html delete mode 100644 project/core/templates/static/js/settings.js diff --git a/project/accounts/templates/accounts/settings.html b/project/accounts/templates/accounts/settings.html index feece260..a0e4a021 100644 --- a/project/accounts/templates/accounts/settings.html +++ b/project/accounts/templates/accounts/settings.html @@ -1,81 +1,84 @@ {% extends "base.html" %} {% load static %} {% load i18n %} -{% block page_title %}Settings{% endblock page_title %} +{% block page_title %}Settings{% endblock page_title %} {% block extra_css %} - - + + {% endblock extra_css %} -{% block backbone_template %} - - - - - -{% endblock backbone_template %} + -{% block body_class %}grey lighten-4{% endblock body_class %} -{% block content %} - {% include "global_nav.html" %} -
+ {% endblock content %} - -{% block extra_js %} - - -{% endblock extra_js %} diff --git a/project/accounts/templates/accounts/update_settings.html b/project/accounts/templates/accounts/update_settings.html deleted file mode 100644 index a0e4a021..00000000 --- a/project/accounts/templates/accounts/update_settings.html +++ /dev/null @@ -1,84 +0,0 @@ -{% extends "base.html" %} -{% load static %} -{% load i18n %} - -{% block page_title %}Settings{% endblock page_title %} -{% block extra_css %} - - -{% endblock extra_css %} - -{% block content %} -
-
-
-
-
-
- Account Settings -
-
-
- {{form.non_field_errors}} -
- {% csrf_token %} -
-
- {{form.first_name.errors}} - {{form.first_name.label_tag}} - {{form.first_name}} -
-
- {{form.last_name.errors}} - {{form.last_name.label_tag}} - {{form.last_name}} -
-
-
-
- {{form.username.errors}} - {{form.username.label_tag}} - {{form.username}} -
-
- {{form.email.errors}} - {{form.email.label_tag}} - {{form.email}} -
-
-
-
- {{form.about_me.errors}} - {{form.about_me.label_tag}} - {{form.about_me}} -
-
-
- {%if not readonly %} - - {% endif%} -
-
- - -
-
-
-
- - -{% endblock content %} diff --git a/project/accounts/views.py b/project/accounts/views.py index ae21546a..ce380ba2 100644 --- a/project/accounts/views.py +++ b/project/accounts/views.py @@ -80,7 +80,7 @@ class SettingsView(LoginRequiredMixin, UpdateView): login_url = "accounts_login" form_class = ProfileEditForm success_url = reverse_lazy("accounts_settings") - template_name = "accounts/update_settings.html" + template_name = "accounts/settings.html" def get_object(self, queryset=None): return Profile.objects.get(user=self.request.user) diff --git a/project/core/templates/static/js/settings.js b/project/core/templates/static/js/settings.js deleted file mode 100644 index 52e83df1..00000000 --- a/project/core/templates/static/js/settings.js +++ /dev/null @@ -1,84 +0,0 @@ -cw = cw || {}; - -cw.UserModel = BB.Model.extend({ - defaults: function() { - return { - username: "", - email: "", - }; - }, - url: function () { - if (! this.get('username') ) { - throw new Error("This is a race condition! and why we can't have nice things :("); - } - return '/api/account_profile/' + this.get('username') + '/'; - }, - - initialize: function (model, options) { - options = options || {}; - } -}); - - -cw.SettingsView = BB.View.extend({ - - el: '#settings', - - initialize: function(options) { - this.options = options || {}; - - this.template = _.template($('#settings-template').text()); - this.settingsTemplate = _.template($('#settings-base').text()); - this.personalTemplate = _.template($('#settings-personal').text()); - - this.listenTo(this.model, 'change', this.renderAllLabels); - }, - - render: function() { - this.$el.html(this.template()); - - this.$('#settings-el').html(this.settingsTemplate()); - - this.renderPersonal(); - }, - - renderAllLabels:function() { - this.renderPersonal(); - Materialize.updateTextFields(); - }, - - renderPersonal: function() { - this.$('#settings-1').html(this.personalTemplate()); - }, - - events: { - 'blur .save-account': 'saveAccount', - }, - - saveAccount: function (e) { - var $this = $(e.target), - changeKey = $this.attr('id'), - changeVal = $this.val().trim(), - apiData = {}, - _this = this; - - if (this.model.get([changeKey]) === changeVal) { - return; - } - - apiData[changeKey] = changeVal; - - $.ajax({ - url: '/api/edituser/', - type: 'POST', - data: apiData, - success: function () { - Materialize.toast('Saved!', 5000); - - _this.isSave = true; - _this.model.fetch(); - - } - }); - }, -}); From 234137b18b6729782c042f72ecc3431ac9210dc8 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 11 Oct 2022 13:50:23 +0300 Subject: [PATCH 17/63] Remove unused import --- project/accounts/templates/accounts/settings.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/project/accounts/templates/accounts/settings.html b/project/accounts/templates/accounts/settings.html index a0e4a021..df1ffae4 100644 --- a/project/accounts/templates/accounts/settings.html +++ b/project/accounts/templates/accounts/settings.html @@ -79,6 +79,4 @@ - - {% endblock content %} From 40038ae083fc9efd596a1c8ddfde8068aa26b660 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 11 Oct 2022 13:59:15 +0300 Subject: [PATCH 18/63] Remove unused endpoints and tests --- project/accounts/api.py | 41 ++----------------- project/accounts/tests/test_api.py | 63 +++--------------------------- project/accounts/urls/api.py | 3 -- 3 files changed, 9 insertions(+), 98 deletions(-) diff --git a/project/accounts/api.py b/project/accounts/api.py index 66fb1192..0858705a 100644 --- a/project/accounts/api.py +++ b/project/accounts/api.py @@ -139,22 +139,6 @@ def get_permissions(self): return super(ProfileViewSet, self).get_permissions() -# @api_view(["GET"]) -# def get_user(request, username): -# """ -# USAGE: -# This is used to get a user -# """ - -# try: -# user = get_user_model().objects.get(username=username) -# return JsonResponse(UserSerializer(user).data) -# except get_user_model().DoesNotExist: -# return JsonResponse( -# {"error": f"User with username {username} not found"}, status=400 -# ) - - @api_view(["GET"]) def get_profile(request, username): """ @@ -167,6 +151,9 @@ def get_profile(request, username): profile = user.profile result = Profile.objects.summarize(profile) result["issues"] = [] + + # TODO: move this to a property of the user + # lines from voted solutions through result issues append voted_solutions = Activity.objects.filter( user=user.id, civi__c_type="solution", activity_type__contains="pos" ) @@ -213,28 +200,6 @@ def get_profile(request, username): ) -# @api_view(["GET"]) -# def get_card(request, username): -# """ -# USAGE: -# This is used to get a card -# """ - -# try: -# user = get_user_model().objects.get(username=username) -# profile = user.profile -# result = Profile.objects.card_summarize( -# profile, Profile.objects.get(user=request.user) -# ) -# return JsonResponse(result) -# except get_user_model().DoesNotExist: -# return JsonResponse( -# {"error": f"User with username {username} not found"}, status=400 -# ) -# except Exception as e: -# return HttpResponseBadRequest(reason=str(e)) - - def get_feed(request): """ USAGE: diff --git a/project/accounts/tests/test_api.py b/project/accounts/tests/test_api.py index 64af5156..fca55533 100644 --- a/project/accounts/tests/test_api.py +++ b/project/accounts/tests/test_api.py @@ -1,12 +1,13 @@ import json -from PIL import Image -from django.core.files.temp import NamedTemporaryFile + +from accounts.models import Profile +from categories.models import Category from django.contrib.auth import get_user_model +from django.core.files.temp import NamedTemporaryFile from django.test import TestCase -from rest_framework.test import APIClient from django.urls import reverse -from accounts.models import Profile -from categories.models import Category +from PIL import Image +from rest_framework.test import APIClient from threads.models import Civi, Thread @@ -167,58 +168,6 @@ def test_nonexistent_user_account_data(self): self.assertIn("not found", content["error"]) -class GetProfileTests(BaseTestCase): - """A class to test get_profile function""" - - def test_existing_user_profile_data(self): - """Whether a user profile is retrieved""" - - self.client.login(username="newuser", password="password123") - response = self.client.get(reverse("get_profile", args=["newuser"])) - content = json.loads(response.content) - self.assertEqual(response.status_code, 200) - self.assertEqual(content["username"], self.user.username) - - def test_nonexistent_user_profile_data(self): - """Whether retrieving a nonexistent user profile raises 404""" - - self.client.login(username="newuser", password="password123") - response = self.client.get( - reverse("get_profile", args=["newuser" + "not_exist"]) - ) - content = json.loads(response.content) - self.assertEqual(response.status_code, 400) - self.assertIn("not found", content["error"]) - - -class GetCardTests(BaseTestCase): - """A class to test get_card function""" - - def test_existing_user_profile_data(self): - """Whether a user card is retrieved""" - - self.client.login(username="newuser", password="password123") - response = self.client.get(reverse("get_card", args=["newuser"])) - content = json.loads(response.content) - self.assertEqual(response.status_code, 200) - self.assertEqual(content["username"], self.user.username) - self.assertFalse(content["follow_state"]) - response = self.client.get(reverse("get_card", args=["superuser"])) - content = json.loads(response.content) - self.assertEqual(response.status_code, 200) - self.assertEqual(content["username"], self.superuser.username) - self.assertTrue(content["follow_state"]) - - def test_nonexistent_user_profile_data(self): - """Whether retrieving a nonexistent user card raises 404""" - - self.client.login(username="newuser", password="password123") - response = self.client.get(reverse("get_card", args=["newuser" + "not_exist"])) - content = json.loads(response.content) - self.assertEqual(response.status_code, 400) - self.assertIn("not found", content["error"]) - - class EditUserTests(BaseTestCase): """A class to test edit_user function""" diff --git a/project/accounts/urls/api.py b/project/accounts/urls/api.py index dae92f2c..0f45354a 100644 --- a/project/accounts/urls/api.py +++ b/project/accounts/urls/api.py @@ -2,9 +2,6 @@ from django.urls import path urlpatterns = [ - # path("account_data//", api.get_user, name="get_user"), - path("account_profile//", api.get_profile, name="get_profile"), - # path("account_card//", api.get_card, name="get_card"), path("feed/", api.get_feed, name="get_feed"), path("edituser/", api.edit_user, name="edit_user"), path("deleteuser/", api.delete_user, name="delete_user"), From c92bb88e2e9001a9e8af28a935d6cf7bbf6e75ec Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 11 Oct 2022 14:09:16 +0300 Subject: [PATCH 19/63] Remove clear_profile view --- project/accounts/api.py | 23 ----------------------- project/accounts/urls/api.py | 1 - 2 files changed, 24 deletions(-) diff --git a/project/accounts/api.py b/project/accounts/api.py index 0858705a..c7541e10 100644 --- a/project/accounts/api.py +++ b/project/accounts/api.py @@ -283,29 +283,6 @@ def upload_profile_image(request): return HttpResponseForbidden("allowed only via POST") -# @login_required -# def clear_profile_image(request): -# """This function is used to delete a profile image""" - -# if request.method == "POST": -# try: -# account = Profile.objects.get(user=request.user) - -# # Clean up previous image -# account.profile_image.delete() -# account.save() - -# return HttpResponse("Image Deleted") -# except get_user_model().DoesNotExist: -# return HttpResponseServerError( -# reason=f"Profile with id:{request.user.username} does not exist" -# ) -# except Exception: -# return HttpResponseServerError(reason=str("default")) -# else: -# return HttpResponseForbidden("allowed only via POST") - - @login_required @require_post_params(params=["target"]) def request_follow(request): diff --git a/project/accounts/urls/api.py b/project/accounts/urls/api.py index 0f45354a..0928868e 100644 --- a/project/accounts/urls/api.py +++ b/project/accounts/urls/api.py @@ -6,7 +6,6 @@ path("edituser/", api.edit_user, name="edit_user"), path("deleteuser/", api.delete_user, name="delete_user"), path("upload_profile/", api.upload_profile_image, name="upload_profile"), - # path("clear_profile/", api.clear_profile_image, name="clear_profile"), path("follow/", api.request_follow, name="follow_user"), path("unfollow/", api.request_unfollow, name="unfollow_user"), path( From ba8e7b4a170d7e05fcbc7e57eb87d0852b1ce766 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 11 Oct 2022 14:09:28 +0300 Subject: [PATCH 20/63] Cleanup unused JS --- .../core/templates/static/js/user-setup.js | 275 ------------------ 1 file changed, 275 deletions(-) delete mode 100644 project/core/templates/static/js/user-setup.js diff --git a/project/core/templates/static/js/user-setup.js b/project/core/templates/static/js/user-setup.js deleted file mode 100644 index dfc07c04..00000000 --- a/project/core/templates/static/js/user-setup.js +++ /dev/null @@ -1,275 +0,0 @@ -cw = cw || {}; - -cw.AccountModel = BB.Model.extend({ - defaults: function() { - return { - username: "", - first_name: "", - last_name: "", - about_me: "" - }; - }, - urlRoot: "/api/v1/accounts/", - - idAttribute: "username", - initialize: function(model, options) { - options = options || {}; - } -}); - -cw.UserSetupView = BB.View.extend({ - el: "#user-setup", - - currentStep: 0, - baseTemplate: _.template($("#setup-base-template").html()), - step0Template: _.template($("#step0-template").html()), - step1Template: _.template($("#step1-template").html()), - step2Template: _.template($("#step2-template").html()), - - initialize: function(options) { - options = options || {}; - - this.listenTo(this.model, "sync", this.render); - return this; - }, - - render: function() { - if (this.currentStep === 0) { - this.$el.empty().append(this.baseTemplate()); - this.$("#step0") - .empty() - .append(this.step0Template()); - this.$("#step1") - .empty() - .append(this.step1Template()) - .toggleClass("hide"); - this.$("#step2") - .empty() - .append(this.step2Template()) - .toggleClass("hide"); - } - }, - - events: { - "click .prev": "prevStep", - "click .next": "nextStep", - "click .finish": "setupUser", - "change .profile-image-pick": "previewImage", - "click .cancel-image": "clearImageField", - "click .upload-image": "toggleFileDialog", - "keypress .about-me": "limitInput", - "input .step1-input": "validateStep1" - }, - - nextStep: function() { - if (this.currentStep === 0) { - this.$("#step0").addClass("hide"); - this.$("#step1").removeClass("hide"); - this.currentStep = 1; - } else if (this.currentStep === 1) { - var first_name = this.$el.find("#first-name").val(); - var last_name = this.$el.find("#last-name").val(); - var about_me = this.$el.find("#about-me").val(); - - if (first_name && last_name && about_me) { - this.$el.find("#step1").addClass("hide"); - this.$el.find("#step2").removeClass("hide"); - this.currentStep = 2; - } else { - Materialize.toast( - 'Please fill out all the fields', - 5000 - ); - } - } - }, - - prevStep: function() { - if (this.currentStep === 2) { - this.$el.find("#step1").removeClass("hide"); - this.$el.find("#step2").addClass("hide"); - this.currentStep = 1; - } - }, - - // INPUT VALIDATION ============================================================ - // limitInput(): limits the about-me input field to 500 characters - limitInput: function(e) { - var max = 500; - var textarea = this.$el.find("#about-me"); - if (e.which < 0x20) { - return; - } - if (textarea.val().length == max) { - e.preventDefault(); - } else if (textarea.val().length > max) { - textarea.val(textarea.val().substring(0, max)); - } - }, - - validateStep1: function() { - var first_name = this.$el - .find("#first-name") - .val() - .trim(); - - var last_name = this.$el - .find("#last-name") - .val() - .trim(); - - var about_me = this.$el - .find("#about-me") - .val() - .trim(); - - if (first_name && last_name && about_me) { - this.model.set({ - first_name: first_name, - last_name: last_name, - about_me: about_me - }); - - this.$el.find(".finish").removeClass("disabled"); - this.$el.find(".help-text.invalid").addClass("hide"); - this.$el.find(".help-text.valid").removeClass("hide"); - } else { - this.$el.find(".finish").addClass("disabled"); - this.$el.find(".help-text.valid").addClass("hide"); - this.$el.find(".help-text.invalid").removeClass("hide"); - } - }, - - // IMAGE SELECTION & PREVIEW =================================================== - // toggleImgButtons(): displays the appropriate buttons based on image state - toggleImgButtons: function() { - this.$el.find(".profile-image-pick").toggleClass("hide"); - this.$el.find(".upload-image").toggleClass("hide"); - this.$el.find(".cancel-image").toggleClass("hide"); - this.$el.find(".preview-image").toggleClass("hide"); - }, - - // toggleFileDialog(): Lets the user choose a different image - toggleFileDialog: function(e) { - e.stopPropagation(); - e.preventDefault(); - this.$el.find("#id_profile_image").trigger("click"); - }, - - // previewImage(): Creates a preview of the current image file chosen - previewImage: function(e) { - var _this = this; - var img = this.$el.find("#id_profile_image"); - if (img.val()) { - this.uploadProfileImg(); - } - }, - - // clearImageField(): clears only the profile image file field - clearImageField: function(e) { - this.clearProfileImg(); - - e.stopPropagation(); - e.preventDefault(); - }, - - // SENDING REQUEST TO SERVER =================================================== - setupUser: function() { - // preserve reference to 'this' for use in ajax callback - var _this = this; - - var first_name = this.model.get("first_name"); - var last_name = this.model.get("last_name"); - var about_me = this.model.get("about_me"); - - if (first_name && last_name && about_me) { - var csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value; - - $.ajax({ - type: "POST", - url: "/api/edituser/", - data: { - csrfmiddlewaretoken: csrftoken, - full_profile: "True", - about_me: about_me, - first_name: first_name, - last_name: last_name, - }, - success: function(data) { - Materialize.toast( - 'Success', - 5000 - ); - _this.nextStep(); - }, - error: function(data) { - Materialize.toast(data.statusText, 5000); - } - }); - } - }, - - uploadProfileImg: function() { - var _this = this; - var formData = new FormData(this.$el.find("#profile_image_form")[0]); - - $.ajax({ - url: "/api/upload_profile/", - type: "POST", - success: function(response) { - var img = _this.$el.find("#id_profile_image"); - var uploaded_image = img[0].files[0]; - if (uploaded_image) { - var preview_image = _this.$el.find(".preview-image"); - preview_image.attr("src", response.profile_image); - - if (_this.$el.find(".preview-image").hasClass("hide")) { - _this.toggleImgButtons(); - _this.$el.find(".loading").addClass("hide"); - _this.$el.find(".placeholder").addClass("hide"); - } - } - - Materialize.toast("Image Uploaded!", 5000); - }, - error: function(response) { - if (response.status === 400) { - Materialize.toast(response.responseJSON.message, 5000, "red"); - } else if (response.status === 500) { - Materialize.toast("Internal Server Error", 5000, "red"); - } else { - Materialize.toast(response.statusText, 5000, "red"); - } - - _this.$el.find(".loading").addClass("hide"); - _this.$el.find(".placeholder").removeClass("hide"); - _this.$el.find("#profile_image_form")[0].reset(); - }, - data: formData, - cache: false, - contentType: false, - processData: false - }); - this.$el.find(".loading").removeClass("hide"); - this.$el.find(".placeholder").addClass("hide"); - return false; - }, - - clearProfileImg: function() { - var _this = this; - $.ajax({ - url: "/api/clear_profile/", - type: "POST", - success: function(e) { - _this.toggleImgButtons(); - _this.$el.find(".loading").addClass("hide"); - _this.$el.find(".placeholder").removeClass("hide"); - _this.$el.find("#profile_image_form")[0].reset(); - Materialize.toast(JSON.stringify(e), 5000); - }, - error: function(e) { - Materialize.toast(JSON.stringify(e), 5000); - } - }); - } -}); From 335a8bd25b57bca443ca73a860a3b0a654cedff0 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 11 Oct 2022 14:14:03 +0300 Subject: [PATCH 21/63] Remove edit_user API endpoint and test --- project/accounts/api.py | 24 ------------------------ project/accounts/tests/test_api.py | 16 ---------------- project/accounts/urls/api.py | 7 ++++++- 3 files changed, 6 insertions(+), 41 deletions(-) diff --git a/project/accounts/api.py b/project/accounts/api.py index c7541e10..263e4b12 100644 --- a/project/accounts/api.py +++ b/project/accounts/api.py @@ -216,30 +216,6 @@ def get_feed(request): return HttpResponseBadRequest(reason=str(e)) -@login_required -def edit_user(request): - """ - Edit Profile Model - """ - - profile = Profile.objects.get(user=request.user) - data = { - "first_name": request.POST.get("first_name", profile.first_name), - "last_name": request.POST.get("last_name", profile.last_name), - "about_me": request.POST.get("about_me", profile.about_me), - } - - profile.__dict__.update(data) - try: - profile.save() - except Exception as e: - return HttpResponseServerError(reason=str(e)) - - profile.refresh_from_db() - - return JsonResponse(Profile.objects.summarize(profile)) - - @login_required def upload_profile_image(request): """This function is used to allow users to upload profile photos""" diff --git a/project/accounts/tests/test_api.py b/project/accounts/tests/test_api.py index fca55533..16aea831 100644 --- a/project/accounts/tests/test_api.py +++ b/project/accounts/tests/test_api.py @@ -168,22 +168,6 @@ def test_nonexistent_user_account_data(self): self.assertIn("not found", content["error"]) -class EditUserTests(BaseTestCase): - """A class to test edit_user function""" - - def test_first_name_last_name_about_fields_can_be_editable(self): - """Whether first_name, last_name and about_me fields can be edited""" - - self.client.login(username="newuser", password="password123") - data = {"first_name": "First", "last_name": "Last", "about_me": "About me"} - response = self.client.post(reverse("edit_user"), data=data) - self.user.profile.refresh_from_db() - self.assertEqual(response.status_code, 200) - self.assertEqual(self.user.profile.first_name, "First") - self.assertEqual(self.user.profile.last_name, "Last") - self.assertEqual(self.user.profile.about_me, "About me") - - class DeleteUserTests(BaseTestCase): def test_delete_user_removes_from_database(self): self.delete_dummy = get_user_model().objects.create_user( diff --git a/project/accounts/urls/api.py b/project/accounts/urls/api.py index 0928868e..35256e8d 100644 --- a/project/accounts/urls/api.py +++ b/project/accounts/urls/api.py @@ -2,12 +2,17 @@ from django.urls import path urlpatterns = [ + # TODO: port to Django view path("feed/", api.get_feed, name="get_feed"), - path("edituser/", api.edit_user, name="edit_user"), + # TODO: port to Django view path("deleteuser/", api.delete_user, name="delete_user"), + # TODO: port to Django view path("upload_profile/", api.upload_profile_image, name="upload_profile"), + # TODO: port to Django view path("follow/", api.request_follow, name="follow_user"), + # TODO: port to Django view path("unfollow/", api.request_unfollow, name="unfollow_user"), + # TODO: port to Django view path( "edit_user_categories/", api.edit_user_categories, name="edit_user_categories" ), From c0361a37485d4578e9695f6ee499669aed9678c8 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 11 Oct 2022 14:17:41 +0300 Subject: [PATCH 22/63] Small todo --- project/accounts/templates/accounts/feed.html | 1 + 1 file changed, 1 insertion(+) diff --git a/project/accounts/templates/accounts/feed.html b/project/accounts/templates/accounts/feed.html index 93c17a33..3a57943a 100644 --- a/project/accounts/templates/accounts/feed.html +++ b/project/accounts/templates/accounts/feed.html @@ -247,6 +247,7 @@ {% endblock content %} {% block extra_js %} + +{% endblock extra_js %} diff --git a/project/accounts/urls/api.py b/project/accounts/urls/api.py index 35256e8d..1054b32f 100644 --- a/project/accounts/urls/api.py +++ b/project/accounts/urls/api.py @@ -5,8 +5,6 @@ # TODO: port to Django view path("feed/", api.get_feed, name="get_feed"), # TODO: port to Django view - path("deleteuser/", api.delete_user, name="delete_user"), - # TODO: port to Django view path("upload_profile/", api.upload_profile_image, name="upload_profile"), # TODO: port to Django view path("follow/", api.request_follow, name="follow_user"), diff --git a/project/accounts/urls/urls.py b/project/accounts/urls/urls.py index 4702406b..0fa72ef0 100644 --- a/project/accounts/urls/urls.py +++ b/project/accounts/urls/urls.py @@ -7,6 +7,7 @@ RegisterView, SettingsView, UserProfileView, + expunge_user, ) from django.contrib.auth import views as auth_views from django.urls import path @@ -46,4 +47,5 @@ PasswordResetCompleteView.as_view(), name="accounts_password_reset_complete", ), + path("accounts/expunge/", expunge_user, name="expunge_user"), ] diff --git a/project/accounts/views.py b/project/accounts/views.py index ce380ba2..8cdd3d8a 100644 --- a/project/accounts/views.py +++ b/project/accounts/views.py @@ -10,9 +10,10 @@ from django.conf import settings from django.contrib.auth import get_user_model, login from django.contrib.auth import views as auth_views +from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.sites.shortcuts import get_current_site -from django.shortcuts import get_object_or_404 +from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse from django.urls import reverse_lazy from django.utils.encoding import force_str @@ -159,3 +160,37 @@ def get(self, request, username=None): "user": user, }, ) + + +@login_required +def expunge_user(request): + """ + Delete User Information + """ + + user_model = get_user_model() + user = get_object_or_404(user_model, username=request.user.username) + + profile = get_object_or_404(Profile, user=user) + + # Expunge personally identifiable data in user + expunged_user_data = { + "is_active": False, + "email": "", + "first_name": "", + "last_name": "", + "username": f"expunged-{ user.id }", + } + user.__dict__.update(expunged_user_data) + user.save() + + # Expunge personally identifiable data in profile + expunged_profile_data = { + "first_name": "", + "last_name": "", + "about_me": "", + } + profile.__dict__.update(expunged_profile_data) + profile.save() + + return redirect("/") From d58b28f683eaa352835080a2a6fe969b4042c99b Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Wed, 12 Oct 2022 09:58:20 +0300 Subject: [PATCH 24/63] Remove ProfileManager --- project/accounts/models.py | 74 ++++---------------------------------- 1 file changed, 7 insertions(+), 67 deletions(-) diff --git a/project/accounts/models.py b/project/accounts/models.py index 93e285e2..462c033a 100644 --- a/project/accounts/models.py +++ b/project/accounts/models.py @@ -1,16 +1,16 @@ -from django.contrib.auth.models import AbstractUser -import os import io -from django.core.files.storage import default_storage +import os + +from categories.models import Category +from common.utils import PathAndRename from django.conf import settings +from django.contrib.auth.models import AbstractUser +from django.core.files.storage import default_storage +from django.core.files.uploadedfile import InMemoryUploadedFile from django.db import models from PIL import Image, ImageOps -from django.core.files.uploadedfile import InMemoryUploadedFile from taggit.managers import TaggableManager -from categories.models import Category -from common.utils import PathAndRename - class User(AbstractUser): """ @@ -29,65 +29,6 @@ class Meta: WHITE_BG = (255, 255, 255) -class ProfileManager(models.Manager): - def summarize(self, profile): - from threads.models import Civi - - data = { - "username": profile.user.username, - "first_name": profile.first_name, - "last_name": profile.last_name, - "about_me": profile.about_me, - "history": [ - Civi.objects.serialize(c) - for c in Civi.objects.filter(author_id=profile.id).order_by("-created") - ], - "profile_image": profile.profile_image_url, - "followers": self.followers(profile), - "following": self.following(profile), - } - return data - - def chip_summarize(self, profile): - data = { - "username": profile.user.username, - "first_name": profile.first_name, - "last_name": profile.last_name, - "profile_image": profile.profile_image_url, - } - return data - - def card_summarize(self, profile, request_profile): - # Length at which to truncate 'about me' text - about_me_truncate_length = 150 - - # If 'about me' text is longer than 150 characters... add elipsis (truncate) - ellipsis_if_too_long = ( - "" if len(profile.about_me) <= about_me_truncate_length else "..." - ) - - data = { - "id": profile.user.id, - "username": profile.user.username, - "first_name": profile.first_name, - "last_name": profile.last_name, - "about_me": profile.about_me[:about_me_truncate_length] - + ellipsis_if_too_long, - "profile_image": profile.profile_image_url, - "follow_state": True - if profile in request_profile.following.all() - else False, - "request_profile": request_profile.first_name, - } - return data - - def followers(self, profile): - return [self.chip_summarize(follower) for follower in profile.followers.all()] - - def following(self, profile): - return [self.chip_summarize(following) for following in profile.following.all()] - - class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile") first_name = models.CharField(max_length=63, blank=False) @@ -109,7 +50,6 @@ class Profile(models.Model): is_verified = models.BooleanField(default=False) full_profile = models.BooleanField(default=False) - objects = ProfileManager() profile_image = models.ImageField( upload_to=PathAndRename("profile_uploads"), blank=True, null=True ) From c9557832c8a0d45a786274abd1fa2718ce7d84dd Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Wed, 12 Oct 2022 10:00:59 +0300 Subject: [PATCH 25/63] Remove redundant __init__ --- project/accounts/models.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/project/accounts/models.py b/project/accounts/models.py index 462c033a..706597c6 100644 --- a/project/accounts/models.py +++ b/project/accounts/models.py @@ -89,9 +89,6 @@ def profile_image_thumb_url(self): return "/static/img/no_image_md.png" - def __init__(self, *args, **kwargs): - super(Profile, self).__init__(*args, **kwargs) - def save(self, *args, **kwargs): """Image crop/resize and thumbnail creation""" From e3455878e2eb3d8c1e98206d260408ff151fd2e5 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Wed, 12 Oct 2022 10:02:16 +0300 Subject: [PATCH 26/63] Add TODO --- project/accounts/models.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/project/accounts/models.py b/project/accounts/models.py index 706597c6..10cece9e 100644 --- a/project/accounts/models.py +++ b/project/accounts/models.py @@ -104,6 +104,12 @@ def resize_profile_image(self): """ Resizes and crops the user uploaded image and creates a thumbnail version of it """ + + # TODO: try to remove this resize_profile_image method + # or find a more simple way to acheive the goal(s) + # - less disk space? + # - desired shape? + profile_image = Image.open(self.profile_image) # Resize image profile_image = ImageOps.fit( From 52bda913315d397376d7a26c93725775c9fdc75c Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Wed, 12 Oct 2022 10:18:10 +0300 Subject: [PATCH 27/63] Move issues code to User model --- project/accounts/api.py | 37 ++-------------------------- project/accounts/models.py | 50 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 35 deletions(-) diff --git a/project/accounts/api.py b/project/accounts/api.py index e2d0d8ca..7e12cd11 100644 --- a/project/accounts/api.py +++ b/project/accounts/api.py @@ -20,7 +20,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet -from threads.models import Activity, Civi, Thread +from threads.models import Thread from threads.serializers import CiviSerializer, ThreadSerializer from threads.utils import json_response @@ -150,40 +150,7 @@ def get_profile(request, username): user = get_user_model().objects.get(username=username) profile = user.profile result = Profile.objects.summarize(profile) - result["issues"] = [] - - # TODO: move this to a property of the user - # lines from voted solutions through result issues append - voted_solutions = Activity.objects.filter( - user=user.id, civi__c_type="solution", activity_type__contains="pos" - ) - - solution_threads = voted_solutions.values("thread__id").distinct() - for thread_id in solution_threads: - thread = Thread.objects.get(id=thread_id) - solutions = [] - solution_civis = voted_solutions.filter(thread=thread_id).values_list( - "civi__id", flat=True - ) - for civi_id in solution_civis: - c = Civi.objects.get(id=civi_id) - vote = voted_solutions.get(civi__id=civi_id).activity_type - vote_types = {"vote_pos": "Agree", "vote_vpos": "Strongly Agree"} - solution_item = { - "id": c.id, - "title": c.title, - "body": c.body, - "user_vote": vote_types.get(vote), - } - solutions.append(solution_item) - - my_issue_item = { - "thread_id": thread.id, - "thread_title": thread.title, - "category": thread.category.name, - "solutions": solutions, - } - result["issues"].append(my_issue_item) + result["issues"] = user.issues if request.user.username != username: requested_profile = Profile.objects.get(user=request.user) diff --git a/project/accounts/models.py b/project/accounts/models.py index 10cece9e..fb4737d0 100644 --- a/project/accounts/models.py +++ b/project/accounts/models.py @@ -10,6 +10,7 @@ from django.db import models from PIL import Image, ImageOps from taggit.managers import TaggableManager +from threads.models import Activity, Civi, Thread class User(AbstractUser): @@ -22,6 +23,55 @@ class User(AbstractUser): class Meta: db_table = "users" + @property + def issues(self): + """ + TODO: add descriptive docstring and determine a good function name. + TODO: see if this code can be more succinct and optimized. + """ + + issues = [] + + voted_solutions = self.upvoted_solutions + + solution_threads = voted_solutions.values("thread__id").distinct() + for thread_id in solution_threads: + thread = Thread.objects.get(id=thread_id) + solutions = [] + solution_civis = voted_solutions.filter(thread=thread_id).values_list( + "civi__id", flat=True + ) + for civi_id in solution_civis: + c = Civi.objects.get(id=civi_id) + vote = voted_solutions.get(civi__id=civi_id).activity_type + vote_types = {"vote_pos": "Agree", "vote_vpos": "Strongly Agree"} + solution_item = { + "id": c.id, + "title": c.title, + "body": c.body, + "user_vote": vote_types.get(vote), + } + solutions.append(solution_item) + + my_issue_item = { + "thread_id": thread.id, + "thread_title": thread.title, + "category": thread.category.name, + "solutions": solutions, + } + issues.append(my_issue_item) + + return issues + + @property + def upvoted_solutions(self): + """ + Return solutions that this user has given a positive vote. + """ + return Activity.objects.filter( + user=self.id, civi__c_type="solution", activity_type__contains="pos" + ) + # Image manipulation constants PROFILE_IMG_SIZE = (171, 171) From 939717283b1a9075f2499a5df30d55b49b1d3294 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Wed, 12 Oct 2022 10:20:56 +0300 Subject: [PATCH 28/63] Remove get_feed and get_profile --- project/accounts/api.py | 47 +----------------------------------- project/accounts/urls/api.py | 2 -- 2 files changed, 1 insertion(+), 48 deletions(-) diff --git a/project/accounts/api.py b/project/accounts/api.py index 7e12cd11..ac14d1fa 100644 --- a/project/accounts/api.py +++ b/project/accounts/api.py @@ -16,13 +16,12 @@ ) from django.shortcuts import get_object_or_404 from notifications.signals import notify -from rest_framework.decorators import action, api_view +from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet from threads.models import Thread from threads.serializers import CiviSerializer, ThreadSerializer -from threads.utils import json_response class ProfileViewSet(ModelViewSet): @@ -139,50 +138,6 @@ def get_permissions(self): return super(ProfileViewSet, self).get_permissions() -@api_view(["GET"]) -def get_profile(request, username): - """ - USAGE: - This is used to get a user profile - """ - - try: - user = get_user_model().objects.get(username=username) - profile = user.profile - result = Profile.objects.summarize(profile) - result["issues"] = user.issues - - if request.user.username != username: - requested_profile = Profile.objects.get(user=request.user) - if username in requested_profile.following.all(): - result["follow_state"] = True - else: - result["follow_state"] = False - - return JsonResponse(result) - - except get_user_model().DoesNotExist: - return JsonResponse( - {"error": f"User with username {username} not found"}, status=400 - ) - - -def get_feed(request): - """ - USAGE: - This is used to get a feed for a user - """ - try: - feed_threads = [ - Thread.objects.summarize(t) for t in Thread.objects.order_by("-created") - ] - - return json_response(feed_threads) - - except Exception as e: - return HttpResponseBadRequest(reason=str(e)) - - @login_required def upload_profile_image(request): """This function is used to allow users to upload profile photos""" diff --git a/project/accounts/urls/api.py b/project/accounts/urls/api.py index 1054b32f..dd861e86 100644 --- a/project/accounts/urls/api.py +++ b/project/accounts/urls/api.py @@ -2,8 +2,6 @@ from django.urls import path urlpatterns = [ - # TODO: port to Django view - path("feed/", api.get_feed, name="get_feed"), # TODO: port to Django view path("upload_profile/", api.upload_profile_image, name="upload_profile"), # TODO: port to Django view From ae4a5e142daedf29f3b14cc5ffa9041cf4a1ac7a Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Wed, 12 Oct 2022 10:24:18 +0300 Subject: [PATCH 29/63] Remove ProfileActivationView --- project/accounts/tests/test_views.py | 51 ++-------------------------- project/accounts/urls/urls.py | 6 ---- project/accounts/views.py | 50 +-------------------------- 3 files changed, 4 insertions(+), 103 deletions(-) diff --git a/project/accounts/tests/test_views.py b/project/accounts/tests/test_views.py index c3741da6..0e3b270b 100644 --- a/project/accounts/tests/test_views.py +++ b/project/accounts/tests/test_views.py @@ -1,9 +1,8 @@ +from accounts.views import RegisterView from django.contrib.auth import get_user_model -from django.test import TestCase -from django.urls import reverse, resolve from django.contrib.auth import views as auth_views -from accounts.models import Profile -from accounts.views import RegisterView +from django.test import TestCase +from django.urls import resolve, reverse class BaseTestCase(TestCase): @@ -153,50 +152,6 @@ def test_anonymous_users_are_redirected_to_login_page(self): ) -class ProfileActivationViewTests(TestCase): - """A class to test profile activation view""" - - def setUp(self) -> None: - self.response = self.client.post( - reverse("accounts_register"), - { - "username": "newuser", - "email": "newuser@email.com", - "password": "password123", - }, - ) - self.user = get_user_model().objects.get(username="newuser") - self.profile = Profile.objects.get(user=self.user) - self.activation_link = self.response.context[0]["link"] - - def test_activation_link(self): - """Whether the activation link works as expected""" - - self.assertFalse(self.profile.is_verified) - response = self.client.get(self.activation_link) - self.profile.refresh_from_db() - self.assertTrue(self.profile.is_verified) - self.assertTemplateUsed(response, "general_message.html") - self.assertContains(response, "Email Verification Successful") - - def test_activation_link_with_a_verified_user(self): - """Whether a verified user is welcomed by already verified page""" - - self.client.get(self.activation_link) - response = self.client.get(self.activation_link) - self.assertTemplateUsed(response, "general_message.html") - self.assertContains(response, "Email Already Verified") - - def test_invalid_action_link(self): - """Whether a verified user is welcomed by verification error page""" - - invalid_link = self.activation_link[:-10] + "12345/" - response = self.client.get(invalid_link) - self.assertFalse(self.profile.is_verified) - self.assertTemplateUsed(response, "general_message.html") - self.assertContains(response, "Email Verification Error") - - class UserProfileView(BaseTestCase): """A class to test user profile view""" diff --git a/project/accounts/urls/urls.py b/project/accounts/urls/urls.py index 0fa72ef0..df73f6b1 100644 --- a/project/accounts/urls/urls.py +++ b/project/accounts/urls/urls.py @@ -3,7 +3,6 @@ PasswordResetConfirmView, PasswordResetDoneView, PasswordResetView, - ProfileActivationView, RegisterView, SettingsView, UserProfileView, @@ -22,11 +21,6 @@ path("register/", RegisterView.as_view(), name="accounts_register"), path("settings/", SettingsView.as_view(), name="accounts_settings"), path("profile//", UserProfileView.as_view(), name="profile"), - path( - "activate_account///", - ProfileActivationView.as_view(), - name="accounts_activate", - ), path( "accounts/password_reset/", PasswordResetView.as_view(), diff --git a/project/accounts/views.py b/project/accounts/views.py index 8cdd3d8a..364c9ec0 100644 --- a/project/accounts/views.py +++ b/project/accounts/views.py @@ -4,7 +4,7 @@ This module will include views for the accounts app. """ -from accounts.authentication import account_activation_token, send_activation_email +from accounts.authentication import send_activation_email from accounts.forms import ProfileEditForm, UserRegistrationForm from accounts.models import Profile from django.conf import settings @@ -16,8 +16,6 @@ from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse from django.urls import reverse_lazy -from django.utils.encoding import force_str -from django.utils.http import urlsafe_base64_decode from django.views import View from django.views.generic.edit import FormView, UpdateView @@ -100,52 +98,6 @@ def get_initial(self): return super(SettingsView, self).get_initial() -class ProfileActivationView(View): - """ - This shows different views to the user when they are verifying - their account based on whether they are already verified or not. - """ - - def get(self, request, uidb64, token): - - try: - uid = force_str(urlsafe_base64_decode(uidb64)) - user = get_user_model().objects.get(pk=uid) - - except (TypeError, ValueError, OverflowError, get_user_model().DoesNotExist): - user = None - - if user is not None and account_activation_token.check_token(user, token): - profile = user.profile - if profile.is_verified: - redirect_link = {"href": "/", "label": "Back to Main"} - template_var = { - "title": "Email Already Verified", - "content": "You have already verified your email", - "link": redirect_link, - } - else: - profile.is_verified = True - profile.save() - - redirect_link = {"href": "/", "label": "Back to Main"} - template_var = { - "title": "Email Verification Successful", - "content": "Thank you for verifying your email with CiviWiki", - "link": redirect_link, - } - else: - # invalid link - redirect_link = {"href": "/", "label": "Back to Main"} - template_var = { - "title": "Email Verification Error", - "content": "Email could not be verified", - "link": redirect_link, - } - - return TemplateResponse(request, "general_message.html", template_var) - - class UserProfileView(LoginRequiredMixin, View): """A view that shows profile for authorized users""" From a69abea6d8d27d61e439131fdf23c453ff3aa0d4 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Wed, 12 Oct 2022 10:48:29 +0300 Subject: [PATCH 30/63] Avoid circular dependencies --- project/accounts/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/project/accounts/models.py b/project/accounts/models.py index fb4737d0..fa54125b 100644 --- a/project/accounts/models.py +++ b/project/accounts/models.py @@ -10,7 +10,7 @@ from django.db import models from PIL import Image, ImageOps from taggit.managers import TaggableManager -from threads.models import Activity, Civi, Thread +from threads.models import Activity class User(AbstractUser): @@ -29,6 +29,8 @@ def issues(self): TODO: add descriptive docstring and determine a good function name. TODO: see if this code can be more succinct and optimized. """ + # Avoid circular dependencies + from threads.models import Civi, Thread issues = [] From 530f073b1b0ae931c8c4626c484ff4b470dbc409 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Wed, 12 Oct 2022 10:49:06 +0300 Subject: [PATCH 31/63] Add related_name for all ForeignKeys --- ...ity_civi_alter_activity_thread_and_more.py | 105 ++++++++++++++++++ project/threads/migrations/max_migration.txt | 2 +- project/threads/models.py | 66 +++++++++-- 3 files changed, 160 insertions(+), 13 deletions(-) create mode 100644 project/threads/migrations/0007_alter_activity_civi_alter_activity_thread_and_more.py diff --git a/project/threads/migrations/0007_alter_activity_civi_alter_activity_thread_and_more.py b/project/threads/migrations/0007_alter_activity_civi_alter_activity_thread_and_more.py new file mode 100644 index 00000000..915c6cf6 --- /dev/null +++ b/project/threads/migrations/0007_alter_activity_civi_alter_activity_thread_and_more.py @@ -0,0 +1,105 @@ +# Generated by Django 4.1.2 on 2022-10-12 07:45 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("categories", "0002_alter_category_options"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("threads", "0006_alter_civi_linked_civis"), + ] + + operations = [ + migrations.AlterField( + model_name="activity", + name="civi", + field=models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="activities", + to="threads.civi", + ), + ), + migrations.AlterField( + model_name="activity", + name="thread", + field=models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="activities", + to="threads.thread", + ), + ), + migrations.AlterField( + model_name="activity", + name="user", + field=models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="activities", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="rebuttal", + name="author", + field=models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="rebuttals", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="rebuttal", + name="response", + field=models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="rebuttals", + to="threads.response", + ), + ), + migrations.AlterField( + model_name="response", + name="author", + field=models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="responses", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="thread", + name="author", + field=models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="threads", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="thread", + name="category", + field=models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="threads", + to="categories.category", + ), + ), + ] diff --git a/project/threads/migrations/max_migration.txt b/project/threads/migrations/max_migration.txt index b866b231..3b7302ff 100644 --- a/project/threads/migrations/max_migration.txt +++ b/project/threads/migrations/max_migration.txt @@ -1 +1 @@ -0006_alter_civi_linked_civis +0007_alter_activity_civi_alter_activity_thread_and_more diff --git a/project/threads/models.py b/project/threads/models.py index 219142dc..917a6e80 100644 --- a/project/threads/models.py +++ b/project/threads/models.py @@ -4,15 +4,15 @@ import os from calendar import month_name +from categories.models import Category from common.utils import PathAndRename from core.constants import CIVI_TYPES from django.conf import settings +from django.contrib.auth import get_user_model from django.core.files.storage import default_storage from django.core.serializers.json import DjangoJSONEncoder from django.db import models -from django.contrib.auth import get_user_model from taggit.managers import TaggableManager -from categories.models import Category class Fact(models.Model): @@ -64,10 +64,18 @@ def filter_by_category(self, categories): class Thread(models.Model): author = models.ForeignKey( - get_user_model(), default=None, null=True, on_delete=models.PROTECT + get_user_model(), + default=None, + null=True, + on_delete=models.PROTECT, + related_name="threads", ) category = models.ForeignKey( - Category, default=None, null=True, on_delete=models.PROTECT + Category, + default=None, + null=True, + on_delete=models.PROTECT, + related_name="threads", ) facts = models.ManyToManyField(Fact) @@ -219,7 +227,11 @@ class Civi(models.Model): on_delete=models.PROTECT, ) thread = models.ForeignKey( - Thread, related_name="civis", default=None, null=True, on_delete=models.PROTECT + Thread, + related_name="civis", + default=None, + null=True, + on_delete=models.PROTECT, ) tags = TaggableManager() @@ -390,7 +402,11 @@ def dict_with_score(self, requested_user_id=None): class Response(models.Model): author = models.ForeignKey( - get_user_model(), default=None, null=True, on_delete=models.PROTECT + get_user_model(), + default=None, + null=True, + on_delete=models.PROTECT, + related_name="responses", ) civi = models.ForeignKey( Civi, @@ -420,7 +436,11 @@ def get_images(self): class CiviImage(models.Model): objects = CiviImageManager() - civi = models.ForeignKey(Civi, related_name="images", on_delete=models.PROTECT) + civi = models.ForeignKey( + Civi, + related_name="images", + on_delete=models.PROTECT, + ) title = models.CharField(max_length=255, null=True, blank=True) image = models.ImageField( upload_to=PathAndRename("civi_uploads"), null=True, blank=True @@ -453,12 +473,26 @@ def votes(self, civi_id): class Activity(models.Model): user = models.ForeignKey( - get_user_model(), default=None, null=True, on_delete=models.PROTECT + get_user_model(), + default=None, + null=True, + on_delete=models.PROTECT, + related_name="activities", ) thread = models.ForeignKey( - Thread, default=None, null=True, on_delete=models.PROTECT + Thread, + default=None, + null=True, + on_delete=models.PROTECT, + related_name="activities", + ) + civi = models.ForeignKey( + Civi, + default=None, + null=True, + on_delete=models.PROTECT, + related_name="activities", ) - civi = models.ForeignKey(Civi, default=None, null=True, on_delete=models.PROTECT) activity_CHOICES = ( ("vote_vneg", "Vote Strongly Disagree"), @@ -489,10 +523,18 @@ class Meta: class Rebuttal(models.Model): author = models.ForeignKey( - get_user_model(), default=None, null=True, on_delete=models.PROTECT + get_user_model(), + default=None, + null=True, + on_delete=models.PROTECT, + related_name="rebuttals", ) response = models.ForeignKey( - Response, default=None, null=True, on_delete=models.PROTECT + Response, + default=None, + null=True, + on_delete=models.PROTECT, + related_name="rebuttals", ) body = models.TextField(max_length=1023) From 1f06ef12d938cb0c483d891a9154f855796859a1 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Wed, 12 Oct 2022 10:51:37 +0300 Subject: [PATCH 32/63] Avoid circular dependencies --- project/accounts/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/project/accounts/models.py b/project/accounts/models.py index fa54125b..0eb9010d 100644 --- a/project/accounts/models.py +++ b/project/accounts/models.py @@ -10,7 +10,6 @@ from django.db import models from PIL import Image, ImageOps from taggit.managers import TaggableManager -from threads.models import Activity class User(AbstractUser): @@ -70,6 +69,9 @@ def upvoted_solutions(self): """ Return solutions that this user has given a positive vote. """ + # Avoid circular dependencies + from threads.models import Activity + return Activity.objects.filter( user=self.id, civi__c_type="solution", activity_type__contains="pos" ) From 11fae1121dd9c2fbf736d0e01b71c2e32b1d0e32 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Wed, 12 Oct 2022 18:29:48 +0300 Subject: [PATCH 33/63] Remove User.issues property --- project/accounts/models.py | 42 -------------------------------------- 1 file changed, 42 deletions(-) diff --git a/project/accounts/models.py b/project/accounts/models.py index 0eb9010d..0adc1f58 100644 --- a/project/accounts/models.py +++ b/project/accounts/models.py @@ -22,48 +22,6 @@ class User(AbstractUser): class Meta: db_table = "users" - @property - def issues(self): - """ - TODO: add descriptive docstring and determine a good function name. - TODO: see if this code can be more succinct and optimized. - """ - # Avoid circular dependencies - from threads.models import Civi, Thread - - issues = [] - - voted_solutions = self.upvoted_solutions - - solution_threads = voted_solutions.values("thread__id").distinct() - for thread_id in solution_threads: - thread = Thread.objects.get(id=thread_id) - solutions = [] - solution_civis = voted_solutions.filter(thread=thread_id).values_list( - "civi__id", flat=True - ) - for civi_id in solution_civis: - c = Civi.objects.get(id=civi_id) - vote = voted_solutions.get(civi__id=civi_id).activity_type - vote_types = {"vote_pos": "Agree", "vote_vpos": "Strongly Agree"} - solution_item = { - "id": c.id, - "title": c.title, - "body": c.body, - "user_vote": vote_types.get(vote), - } - solutions.append(solution_item) - - my_issue_item = { - "thread_id": thread.id, - "thread_title": thread.title, - "category": thread.category.name, - "solutions": solutions, - } - issues.append(my_issue_item) - - return issues - @property def upvoted_solutions(self): """ From f9964a02b15fb545f9fb924a72a1d9657bb0a12b Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Wed, 12 Oct 2022 18:31:34 +0300 Subject: [PATCH 34/63] Remove unused code --- .../templates/static/js/update_settings.js | 38 ------------------- 1 file changed, 38 deletions(-) delete mode 100644 project/core/templates/static/js/update_settings.js diff --git a/project/core/templates/static/js/update_settings.js b/project/core/templates/static/js/update_settings.js deleted file mode 100644 index 691ef2d8..00000000 --- a/project/core/templates/static/js/update_settings.js +++ /dev/null @@ -1,38 +0,0 @@ -$('.cd-popup-yes').on('click', function () { - $.ajax({ - type: "POST", - url: '/api/deleteuser', - data: { - csrfmiddlewaretoken: $("[name=csrfmiddlewaretoken]").val(), - }, - success: function (response) { - window.location.assign("/"); - console.log('success'); - }, - error: function (response) { - console.log('error'); - } - }); -}); -// Confirmation dialog (HTML/CSS/JS) from https://codyhouse.co/gem/simple-confirmation-popup -jQuery(document).ready(function ($) { - //open popup - $('.cd-popup-trigger').on('click', function (event) { - event.preventDefault(); - $('.cd-popup').addClass('is-visible'); - }); - - //close popup - $('.cd-popup').on('click', function (event) { - if ($(event.target).is('.cd-popup-close') || $(event.target).is('.cd-popup') || $(event.target).is('.cd-popup-no')) { - event.preventDefault(); - $(this).removeClass('is-visible'); - } - }); - //close popup when clicking the esc keyboard button - $(document).keyup(function (event) { - if (event.which == '27') { - $('.cd-popup').removeClass('is-visible'); - } - }); -}); \ No newline at end of file From af0aff4b2033bfa4cb7114997c1af84ce90e3998 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Wed, 12 Oct 2022 18:45:16 +0300 Subject: [PATCH 35/63] Add profile image to settings form --- project/accounts/templates/accounts/settings.html | 14 ++++++++++++++ project/accounts/views.py | 1 + 2 files changed, 15 insertions(+) diff --git a/project/accounts/templates/accounts/settings.html b/project/accounts/templates/accounts/settings.html index 44b8f766..4ca5960a 100644 --- a/project/accounts/templates/accounts/settings.html +++ b/project/accounts/templates/accounts/settings.html @@ -34,6 +34,13 @@ {{form.last_name}} +
+
+ {{form.profile_image.errors}} + {{form.profile_image.label_tag}} + {{form.profile_image}} +
+
{{form.username.errors}} @@ -53,6 +60,13 @@ {{form.about_me}}
+
+
+ {{form.categories.errors}} + {{form.categories.label_tag}} + {{form.categories}} +
+
{%if not readonly %} diff --git a/project/accounts/views.py b/project/accounts/views.py index 364c9ec0..a045517c 100644 --- a/project/accounts/views.py +++ b/project/accounts/views.py @@ -93,6 +93,7 @@ def get_initial(self): "first_name": profile.first_name or None, "last_name": profile.last_name or None, "about_me": profile.about_me or None, + "profile_image": profile.profile_image or None, } ) return super(SettingsView, self).get_initial() From f61c4ac31de645b7faad6578c67a1c40924a2dc1 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Wed, 12 Oct 2022 18:49:28 +0300 Subject: [PATCH 36/63] Remove "readonly" settings page --- project/accounts/forms.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/project/accounts/forms.py b/project/accounts/forms.py index 5b3573d8..af0d18db 100644 --- a/project/accounts/forms.py +++ b/project/accounts/forms.py @@ -1,11 +1,13 @@ import re -from django.core.files.images import get_image_dimensions + +from accounts.models import Profile from django import forms -from django.forms.models import ModelForm from django.contrib.auth import get_user_model +from django.core.files.images import get_image_dimensions +from django.forms.models import ModelForm from django.utils.translation import gettext_lazy as _ + from .reserved_usernames import RESERVED_USERNAMES -from accounts.models import Profile class UserRegistrationForm(ModelForm): @@ -106,16 +108,6 @@ class ProfileEditForm(forms.ModelForm): Form for updating Profile data """ - def __init__(self, *args, **kwargs): - readonly = kwargs.pop("readonly", False) - super(ProfileEditForm, self).__init__(*args, **kwargs) - if readonly: - self.disable_fields() - - def disable_fields(self): - for _key, value in self.fields.items(): - value.disabled = True - class Meta: model = Profile fields = [ From 9b82046b5b187053db8333980b65d424628a7554 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Wed, 12 Oct 2022 18:55:41 +0300 Subject: [PATCH 37/63] Add multipart/form-data for image upload --- project/accounts/templates/accounts/settings.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/accounts/templates/accounts/settings.html b/project/accounts/templates/accounts/settings.html index 4ca5960a..8fb956d2 100644 --- a/project/accounts/templates/accounts/settings.html +++ b/project/accounts/templates/accounts/settings.html @@ -20,7 +20,7 @@
{{form.non_field_errors}} -
+ {% csrf_token %}
From 415420dc6ae58df463fd6b7177770cc156b837bf Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Wed, 12 Oct 2022 18:57:37 +0300 Subject: [PATCH 38/63] Fix profile image display --- project/core/templates/global_nav.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/core/templates/global_nav.html b/project/core/templates/global_nav.html index 61959afc..7226ff77 100644 --- a/project/core/templates/global_nav.html +++ b/project/core/templates/global_nav.html @@ -21,7 +21,7 @@ {% if user.is_authenticated %} From 125ffb9181037565164e8dace61380b35edb59f3 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 18 Oct 2022 21:11:33 +0300 Subject: [PATCH 39/63] Remove unused API views/tests --- project/accounts/api.py | 202 +------------- project/accounts/tests/test_api.py | 382 -------------------------- project/accounts/tests/test_models.py | 57 +--- project/accounts/tests/test_views.py | 3 +- project/accounts/urls/api.py | 6 - project/core/router.py | 5 +- 6 files changed, 5 insertions(+), 650 deletions(-) delete mode 100644 project/accounts/tests/test_api.py diff --git a/project/accounts/api.py b/project/accounts/api.py index ac14d1fa..3800beb9 100644 --- a/project/accounts/api.py +++ b/project/accounts/api.py @@ -1,184 +1,9 @@ -from accounts.forms import UpdateProfileImage from accounts.models import Profile -from accounts.permissions import IsProfileOwnerOrDuringRegistrationOrReadOnly -from accounts.serializers import ProfileListSerializer, ProfileSerializer -from accounts.utils import get_account -from categories.models import Category -from categories.serializers import CategorySerializer from core.custom_decorators import require_post_params from django.contrib.auth import get_user_model from django.contrib.auth.decorators import login_required -from django.http import ( - HttpResponseBadRequest, - HttpResponseForbidden, - HttpResponseServerError, - JsonResponse, -) -from django.shortcuts import get_object_or_404 +from django.http import HttpResponseServerError, JsonResponse from notifications.signals import notify -from rest_framework.decorators import action -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from rest_framework.viewsets import ModelViewSet -from threads.models import Thread -from threads.serializers import CiviSerializer, ThreadSerializer - - -class ProfileViewSet(ModelViewSet): - - """ - REST API ViewSet for an Profile - retrieve: - Return the given user based a username. - - list: - Return a list of all the existing user profile. Only with privileged access. - """ - - queryset = Profile.objects.all() - lookup_field = "user__username" - serializer_class = ProfileSerializer - http_method_names = ["get", "head", "put", "patch"] - permission_classes = (IsProfileOwnerOrDuringRegistrationOrReadOnly,) - - def list(self, request): - """ """ - if self.request.user.is_staff: - profiles = Profile.objects.all() - else: - profiles = Profile.objects.filter(user=self.request.user) - serializer = ProfileListSerializer(profiles, many=True) - return Response(serializer.data) - - def retrieve(self, request, user__username=None): - """ """ - profile = get_account(username=user__username) - if self.request.user == profile.user: - serializer = ProfileSerializer(profile) - else: - serializer = ProfileListSerializer(profile) - return Response(serializer.data) - - @action(detail=True) - def civis(self, request, user__username=None): - """ - Gets the civis of the selected user account - /accounts/{username}/civis - """ - user = get_object_or_404(get_user_model(), username=user__username) - user_civis = user.civis - serializer = CiviSerializer(user_civis, many=True) - return Response(serializer.data) - - @action(detail=True) - def followers(self, request, user__username=None): - """ - Gets the followers of the selected user account - /accounts/{username}/followers - """ - profile = get_account(username=user__username) - followers = profile.followers.all() - serializer = ProfileListSerializer(followers, many=True) - return Response(serializer.data) - - @action(detail=True) - def following(self, request, user__username=None): - """ - Gets the followings of the selected user account - /accounts/{username}/following - """ - profile = get_account(username=user__username) - followings = profile.following.all() - serializer = ProfileListSerializer(followings, many=True) - return Response(serializer.data) - - @action(detail=True) - def categories(self, request, user__username=None): - """ - Gets the preferred categories of the selected user account - /accounts/{username}/categories - """ - profile = get_account(username=user__username) - categories = profile.categories - serializer = CategorySerializer(categories, many=True) - return Response(serializer.data) - - @action(detail=True) - def threads(self, request, user__username=None): - """ - Gets the published threads of the selected user account - /accounts/{username}/threads - """ - user = get_user_model().objects.get(username=user__username) - published_threads = Thread.objects.filter(author=user, is_draft=False) - serializer = ThreadSerializer( - published_threads, many=True, context={"request": request} - ) - return Response(serializer.data) - - @action(detail=True) - def drafts(self, request, user__username=None): - """ - Gets the draft threads of the selected account - /accounts/{username}/drafts - """ - user = get_user_model().objects.get(username=user__username) - draft_threads = Thread.objects.filter(author=user, is_draft=True) - serializer = ThreadSerializer( - draft_threads, many=True, context={"request": request} - ) - return Response(serializer.data) - - def get_permissions(self): - if self.action in ["list"]: - self.permission_classes = [ - IsAuthenticated, - IsProfileOwnerOrDuringRegistrationOrReadOnly, - ] - return super(ProfileViewSet, self).get_permissions() - - -@login_required -def upload_profile_image(request): - """This function is used to allow users to upload profile photos""" - - if request.method == "POST": - form = UpdateProfileImage(request.POST, request.FILES) - if form.is_valid(): - try: - profile = Profile.objects.get(user=request.user) - - # Clean up previous image - profile.profile_image.delete() - - # Upload new image and set as profile picture - profile.profile_image = form.clean_profile_image() - try: - profile.save() - except Exception as e: - response = {"message": str(e), "error": "MODEL_SAVE_ERROR"} - return JsonResponse(response, status=400) - - request.session["login_user_image"] = profile.profile_image_thumb_url - - response = {"profile_image": profile.profile_image_url} - return JsonResponse(response, status=200) - - except get_user_model().DoesNotExist: - response = { - "message": f"{request.user.username} does not have profile", - "error": "ACCOUNT_ERROR", - } - return JsonResponse(response, status=400) - except Exception as e: - response = {"message": str(e), "error": "MODEL_ERROR"} - return JsonResponse(response, status=400) - else: - response = {"message": form.errors["profile_image"], "error": "FORM_ERROR"} - return JsonResponse(response, status=400) - - else: - return HttpResponseForbidden("allowed only via POST") @login_required @@ -268,28 +93,3 @@ def request_unfollow(request): ) except Exception as e: return HttpResponseServerError(reason=str(e)) - - -@login_required -def edit_user_categories(request): - """ - USAGE: - Edits list of categories for the user - """ - try: - profile = Profile.objects.get(user=request.user) - categories = [int(i) for i in request.POST.getlist("categories[]")] - profile.categories.clear() - for category in categories: - profile.categories.add(Category.objects.get(id=category)) - profile.save() - - data = { - "user_categories": list(profile.categories.values_list("id", flat=True)) - or "all_categories" - } - return JsonResponse({"result": data}) - except get_user_model().DoesNotExist as e: - return HttpResponseBadRequest(reason=str(e)) - except Exception as e: - return HttpResponseServerError(reason=str(e)) diff --git a/project/accounts/tests/test_api.py b/project/accounts/tests/test_api.py deleted file mode 100644 index 16aea831..00000000 --- a/project/accounts/tests/test_api.py +++ /dev/null @@ -1,382 +0,0 @@ -import json - -from accounts.models import Profile -from categories.models import Category -from django.contrib.auth import get_user_model -from django.core.files.temp import NamedTemporaryFile -from django.test import TestCase -from django.urls import reverse -from PIL import Image -from rest_framework.test import APIClient -from threads.models import Civi, Thread - - -class BaseTestCase(TestCase): - """Base test class to set up test cases""" - - def setUp(self) -> None: - self.client = APIClient() - self.user = get_user_model().objects.create_user( - username="newuser", email="test@test.com", password="password123" - ) - self.user2 = get_user_model().objects.create_user( - username="newuser2", email="test2@test.com", password="password123" - ) - self.superuser = get_user_model().objects.create_superuser( - username="superuser", email="superuser@su.com", password="superpassword123" - ) - self.user.profile.following.add(self.superuser.profile) - self.user.profile.followers.add(self.superuser.profile, self.user2.profile) - self.category = Category.objects.create(name="NewCategory") - self.user.profile.categories.add(self.category) - self.thread = Thread.objects.create( - author=self.user, - title="Thread", - summary="summary", - category=self.category, - is_draft=False, - ) - self.draft_thread = Thread.objects.create( - author=self.user, - title="Draft Thread", - summary="draft_summary", - category=self.category, - ) - self.civi = Civi.objects.create( - author=self.user, thread=self.thread, title="Civi", body="body" - ) - - def tearDown(self) -> None: - self.client.logout() - - -class ProfileViewSetTests(BaseTestCase): - """A class to test ProfileViewSet""" - - def test_anonymous_user_cannot_list_profile(self): - """Whether unauthenticated users cannot list profiles""" - - response = self.client.get(reverse("profile-list")) - self.assertEqual(response.status_code, 401) - - def test_users_can_list_only_their_profiles(self): - """Whether authenticated users can only get their own profiles""" - - self.client.login(username="newuser", password="password123") - response = self.client.get(reverse("profile-list")) - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data), 1) - - def test_superusers_can_list_all_profiles(self): - """Whether superusers can get all profiles""" - - self.client.login(username="superuser", password="superpassword123") - response = self.client.get(reverse("profile-list")) - self.assertEqual(response.status_code, 200) - number_of_profiles = Profile.objects.count() - self.assertEqual(len(response.data), number_of_profiles) - - def test_users_can_retrive_their_profiles_serialized_by_profileserializer(self): - """Whether ProfileSerializer is used when users get their profiles""" - - self.client.login(username="newuser", password="password123") - response = self.client.get(reverse("profile-detail", args=["newuser"])) - self.assertEqual(response.status_code, 200) - self.assertIn("username", response.data) - self.assertIn("email", response.data) - self.assertIn("about_me", response.data) - self.assertIn("is_staff", response.data) - - def test_other_users_profiles_retrived_by_serialized_by_profilelistserializer(self): - """Whether ProfileListSerializer is used when users get their profiles""" - - self.client.login(username="newuser", password="password123") - response = self.client.get(reverse("profile-detail", args=["newuser2"])) - self.assertEqual(response.status_code, 200) - self.assertIn("username", response.data) - self.assertNotIn("email", response.data) - self.assertNotIn("about_me", response.data) - self.assertNotIn("is_staff", response.data) - - def test_only_get_published_threads(self): - """Whether only published threads can be retrieved""" - - response = self.client.get(reverse("profile-threads", args=["newuser"])) - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data), 1) - self.assertNotEqual(response.data[0]["title"], "Draft Thread") - self.assertFalse(response.data[0]["is_draft"]) - - def test_only_get_draft_threads(self): - """Whether only draft threads can be retrieved""" - - response = self.client.get(reverse("profile-drafts", args=["newuser"])) - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data), 1) - self.assertEqual(response.data[0]["title"], "Draft Thread") - self.assertTrue(response.data[0]["is_draft"]) - - def test_get_civis_created_by_a_user(self): - """Whether civis of a user can be retrieved""" - - response = self.client.get(reverse("profile-civis", args=["newuser"])) - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data), 1) - self.assertEqual(response.data[0]["title"], "Civi") - - def test_get_followers(self): - """Whether followers of a user can be retrieved""" - - response = self.client.get(reverse("profile-followers", args=["newuser"])) - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data), self.user.profile.followers.count()) - - def test_get_followings(self): - """Whether followings of a user can be retrieved""" - - response = self.client.get(reverse("profile-following", args=["newuser"])) - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data), self.user.profile.following.count()) - - def test_get_categories_followed_by_a_user(self): - """Whether categories followed by a user can be retrieved""" - - response = self.client.get(reverse("profile-categories", args=["newuser"])) - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data), self.user.profile.categories.count()) - - -class GetUserTests(BaseTestCase): - """A class to test get_user function""" - - def test_existing_user_account_data(self): - """Whether a user is retrieved""" - - self.client.login(username="newuser", password="password123") - response = self.client.get(reverse("get_user", args=["newuser"])) - content = json.loads(response.content) - self.assertEqual(response.status_code, 200) - self.assertEqual(content["username"], self.user.username) - - def test_nonexistent_user_account_data(self): - """Whether retrieving a nonexistent user raises 404""" - - self.client.login(username="newuser", password="password123") - response = self.client.get(reverse("get_user", args=["newuser" + "not_exist"])) - content = json.loads(response.content) - self.assertEqual(response.status_code, 400) - self.assertIn("not found", content["error"]) - - -class DeleteUserTests(BaseTestCase): - def test_delete_user_removes_from_database(self): - self.delete_dummy = get_user_model().objects.create_user( - username="s_delete", - email="test@test.com", - password="password123", - first_name="Bob", - last_name="George", - ) - self.assertEqual(self.delete_dummy.first_name, "Bob") - self.client.login(username="s_delete", password="password123") - self.client.post(reverse("delete_user")) - self.delete_dummy.refresh_from_db() - assert self.delete_dummy.username.startswith("[Deleted-") - self.assertEqual(self.delete_dummy.first_name, "") - self.assertEqual(self.delete_dummy.last_name, "") - self.client.logout() - - def test_delete_user_fails_if_not_logged_in(self): - self.delete_dummy = get_user_model().objects.create_user( - username="f_delete", - email="test@test.com", - password="password123", - first_name="John", - last_name="Smith", - ) - self.assertEqual(self.delete_dummy.first_name, "John") - self.client.post(reverse("delete_user")) - self.delete_dummy.refresh_from_db() - self.assertEqual(self.delete_dummy.username, "f_delete") - self.assertEqual(self.delete_dummy.first_name, "John") - self.assertEqual(self.delete_dummy.last_name, "Smith") - - def test_delete_user_removes_profile_information(self): - self.delete_dummy = get_user_model().objects.create_user( - username="prof", - email="test@test.com", - password="password123", - first_name="Aerith", - last_name="Sans", - ) - self.client.login(username="prof", password="password123") - self.dummy_profile = Profile.objects.get(user=self.delete_dummy) - data = {"first_name": "Aerith", "last_name": "Sans", "about_me": "Dummy"} - self.dummy_profile.__dict__.update(data) - - self.assertEqual(self.dummy_profile.first_name, "Aerith") - self.client.post(reverse("delete_user")) - self.dummy_profile.refresh_from_db() - self.assertEqual(self.dummy_profile.about_me, "") - self.assertEqual(self.dummy_profile.first_name, "") - self.assertEqual(self.dummy_profile.last_name, "") - self.client.logout() - - def test_delete_user_can_delete_reused_usernames(self): - self.delete_dummy = get_user_model().objects.create_user( - username="imposter", - email="test@test.com", - password="password123", - first_name="Among", - last_name="Us", - ) - self.client.login(username="imposter", password="password123") - self.assertEqual(self.delete_dummy.first_name, "Among") - self.client.post(reverse("delete_user")) - self.delete_dummy.refresh_from_db() - self.client.logout() - - self.second_dummy = get_user_model().objects.create_user( - username="imposter", - email="test@test.com", - password="password123", - first_name="Tyler", - last_name="One", - ) - self.client.login(username="imposter", password="password123") - self.assertEqual(self.second_dummy.username, "imposter") - self.assertEqual(self.second_dummy.first_name, "Tyler") - self.client.post(reverse("delete_user")) - self.second_dummy.refresh_from_db() - assert self.second_dummy.username.startswith("[Deleted-") - self.assertEqual(self.second_dummy.first_name, "") - self.assertEqual(self.second_dummy.last_name, "") - self.client.logout() - - -class UploadProfileImage(BaseTestCase): - """A class to test upload_profile_image function""" - - def setUp(self) -> None: - super(UploadProfileImage, self).setUp() - self.client.login(username="newuser", password="password123") - self.url = reverse("upload_profile") - self.image = Image.new("RGB", size=(5, 5), color=(0, 0, 0)) - self.file = NamedTemporaryFile(suffix=".jpg") - self.image.save(self.file) - - def tearDown(self) -> None: - for profile in Profile.objects.all(): - profile.profile_image.delete() - profile.profile_image_thumb.delete() - - def test_upload_profile_image(self): - """Whether upload_profile_image function works as expected""" - - with open(self.file.name, "rb") as image_file: - data = {"profile_image": image_file} - response = self.client.post(self.url, data=data) - content = json.loads(response.content) - self.user.profile.refresh_from_db() - self.assertEqual( - content.get("profile_image"), self.user.profile.profile_image_url - ) - self.assertNotEqual( - self.user.profile.profile_image_url, "/static/img/no_image_md.png" - ) - self.assertNotEqual( - self.user.profile.profile_image_thumb_url, "/static/img/no_image_md.png" - ) - - def test_upload_profile_image_with_invalid_extension(self): - """Whether upload_profile_image raises an error when non-image file is past""" - - image = Image.new("RGB", size=(5, 5), color=(0, 0, 0)) - file = NamedTemporaryFile(suffix=".pdf") - image.save(file) - - with open(file.name, "rb") as image_file: - data = {"profile_image": image_file} - response = self.client.post(self.url, data=data) - content = json.loads(response.content) - self.assertEqual(content["error"], "FORM_ERROR") - self.user.profile.refresh_from_db() - self.assertEqual( - self.user.profile.profile_image_url, "/static/img/no_image_md.png" - ) - self.assertEqual( - self.user.profile.profile_image_thumb_url, "/static/img/no_image_md.png" - ) - - -class RequestFollowTests(BaseTestCase): - """A class to test request_follow function""" - - def test_request_follow(self): - """Whether request_follow function works as expected""" - - self.client.login(username="newuser", password="password123") - number_of_followings = self.user.profile.following.count() - data = {"target": self.user2.username} - response = self.client.post(reverse("follow_user"), data=data) - content = json.loads(response.content) - self.assertEqual(response.status_code, 200) - self.assertEqual(content.get("result").get("username"), self.user2.username) - self.assertTrue(content.get("result").get("follow_status")) - self.assertEqual(self.user.profile.following.count(), number_of_followings + 1) - - def test_user_cannot_follow_itself(self): - """Whether a user trying to follow itself raises an error""" - - self.client.login(username="newuser", password="password123") - data = {"target": self.user.username} - response = self.client.post(reverse("follow_user"), data=data) - content = json.loads(response.content) - self.assertEqual(response.status_code, 400) - self.assertIn("You cannot follow yourself", content.get("error")) - - def test_following_nonexistent_user_gives_error(self): - """Whether following a nonexistent user raises an error""" - - self.client.login(username="newuser", password="password123") - data = {"target": "nonexistent"} - response = self.client.post(reverse("follow_user"), data=data) - content = json.loads(response.content) - self.assertEqual(response.status_code, 400) - self.assertIn("not found", content.get("error")) - - -class RequestUnfollowTests(BaseTestCase): - """A class to test request_unfollow function""" - - def test_request_unfollow(self): - """Whether request_unfollow function works as expected""" - - self.client.login(username="newuser", password="password123") - number_of_followings = self.user.profile.following.count() - data = {"target": self.superuser.username} - response = self.client.post(reverse("unfollow_user"), data=data) - content = json.loads(response.content) - self.assertEqual(response.status_code, 200) - self.assertEqual(content.get("result"), "Success") - self.assertEqual(self.user.profile.following.count(), number_of_followings - 1) - - def test_username_for_unfollowing_cannot_be_empty(self): - """Whether unfollowing an empty username raises an error""" - - self.client.login(username="newuser", password="password123") - data = {"target": ""} - response = self.client.post(reverse("unfollow_user"), data=data) - content = json.loads(response.content) - self.assertEqual(response.status_code, 400) - self.assertEqual(content.get("error"), "username cannot be empty") - - def test_unfollowing_nonexistent_user_gives_error(self): - """Whether unfollowing an nonexistent user raises an error""" - - self.client.login(username="newuser", password="password123") - data = {"target": "nonexistent"} - response = self.client.post(reverse("unfollow_user"), data=data) - content = json.loads(response.content) - self.assertEqual(response.status_code, 400) - self.assertIn("not found", content.get("error")) diff --git a/project/accounts/tests/test_models.py b/project/accounts/tests/test_models.py index 83e4308a..bd481216 100644 --- a/project/accounts/tests/test_models.py +++ b/project/accounts/tests/test_models.py @@ -1,6 +1,6 @@ +from accounts.models import Profile from django.contrib.auth import get_user_model from django.test import TestCase -from accounts.models import Profile class BaseTestCase(TestCase): @@ -10,7 +10,7 @@ def setUp(self) -> None: user = get_user_model().objects.create_user( username="testuser", email="test@test.com", password="password123" ) - self.test_profile, created = Profile.objects.update_or_create( + self.test_profile, _created = Profile.objects.update_or_create( user=user, defaults={ "first_name": "Test", @@ -39,59 +39,6 @@ def test_profile_has_default_image_url(self): ) -class ProfileManagerTests(BaseTestCase): - """A class to test ProfileManager""" - - def test_profile_summarize_with_no_history_no_followers_no_following(self): - """ - Test whether profile summarize is correct - without history, followers, and followings - """ - - data = { - "username": self.test_profile.user.username, - "first_name": self.test_profile.first_name, - "last_name": self.test_profile.last_name, - "about_me": self.test_profile.about_me, - "history": [], - "profile_image": self.test_profile.profile_image_url, - "followers": [], - "following": [], - } - self.assertEqual(Profile.objects.summarize(self.test_profile), data) - - def test_profile_chip_summarize(self): - """Whether profile chip summarize is correct""" - - data = { - "username": self.test_profile.user.username, - "first_name": self.test_profile.first_name, - "last_name": self.test_profile.last_name, - "profile_image": self.test_profile.profile_image_url, - } - self.assertEqual(Profile.objects.chip_summarize(self.test_profile), data) - - def test_profile_card_summarize(self): - """Whether profile card summarize is correct""" - - data = { - "id": self.test_profile.user.id, - "username": self.test_profile.user.username, - "first_name": self.test_profile.first_name, - "last_name": self.test_profile.last_name, - "about_me": self.test_profile.about_me, - "profile_image": self.test_profile.profile_image_url, - "follow_state": False, - "request_profile": self.test_profile.first_name, - } - self.assertEqual( - Profile.objects.card_summarize( - self.test_profile, Profile.objects.get(user=self.test_profile.user) - ), - data, - ) - - class UserModelTest(TestCase): """A class to test Profile model""" diff --git a/project/accounts/tests/test_views.py b/project/accounts/tests/test_views.py index 0e3b270b..a8743f14 100644 --- a/project/accounts/tests/test_views.py +++ b/project/accounts/tests/test_views.py @@ -126,7 +126,7 @@ def setUp(self) -> None: def test_template_name(self): """Whether the correct template is used""" - self.assertTemplateUsed(self.response, "accounts/update_settings.html") + self.assertTemplateUsed(self.response, "accounts/settings.html") def test_contains_existing_data(self): """Whether the existing data is available""" @@ -169,5 +169,4 @@ def test_get_user_profile(self): response = self.client.get(reverse("profile", args=["newuser"])) self.assertEqual(response.status_code, 200) self.assertContains(response, self.user.username) - self.assertContains(response, self.user.email) self.assertTemplateUsed(response, "account.html") diff --git a/project/accounts/urls/api.py b/project/accounts/urls/api.py index dd861e86..7ac1af4b 100644 --- a/project/accounts/urls/api.py +++ b/project/accounts/urls/api.py @@ -2,14 +2,8 @@ from django.urls import path urlpatterns = [ - # TODO: port to Django view - path("upload_profile/", api.upload_profile_image, name="upload_profile"), # TODO: port to Django view path("follow/", api.request_follow, name="follow_user"), # TODO: port to Django view path("unfollow/", api.request_unfollow, name="unfollow_user"), - # TODO: port to Django view - path( - "edit_user_categories/", api.edit_user_categories, name="edit_user_categories" - ), ] diff --git a/project/core/router.py b/project/core/router.py index a7351ced..f9af5de6 100644 --- a/project/core/router.py +++ b/project/core/router.py @@ -1,11 +1,8 @@ -from rest_framework import routers -from accounts.api import ProfileViewSet from categories.api import CategoryViewSet +from rest_framework import routers from threads.views import CiviViewSet, ThreadViewSet - CiviWikiRouter = routers.DefaultRouter() -CiviWikiRouter.register(r"accounts", ProfileViewSet) CiviWikiRouter.register(r"categories", CategoryViewSet) CiviWikiRouter.register(r"threads", ThreadViewSet) CiviWikiRouter.register(r"civis", CiviViewSet) From fde9aaeb15fe1dd2dd69a18adc75c7968cc3faf5 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 18 Oct 2022 21:12:13 +0300 Subject: [PATCH 40/63] Remove profile chip_summarize and unused imprts --- project/threads/api.py | 24 ++++++------ project/threads/views.py | 81 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 90 insertions(+), 15 deletions(-) diff --git a/project/threads/api.py b/project/threads/api.py index f3706651..c998c3e6 100644 --- a/project/threads/api.py +++ b/project/threads/api.py @@ -1,24 +1,22 @@ import json -from notifications.signals import notify from accounts.models import Profile +from common.utils import check_database, save_image_from_url from core.custom_decorators import require_post_params -from common.utils import save_image_from_url -from django.forms.models import model_to_dict +from django.contrib.auth import get_user_model from django.contrib.auth.decorators import login_required -from .models import CiviImage from django.db.models.query import F -from django.contrib.auth import get_user_model +from django.forms.models import model_to_dict from django.http import ( - JsonResponse, - HttpResponseServerError, - HttpResponseForbidden, HttpResponseBadRequest, + HttpResponseForbidden, + HttpResponseServerError, + JsonResponse, ) +from notifications.signals import notify -from .models import Activity, Civi, Thread +from .models import Activity, Civi, CiviImage, Thread from .utils import json_response -from common.utils import check_database @login_required @@ -84,15 +82,15 @@ def get_thread(request, thread_id): "category": model_to_dict(thread.category), "created": thread.created_date_str, "contributors": [ - Profile.objects.chip_summarize(user.profile) + user.profile for user in get_user_model().objects.filter( pk__in=civis.distinct("author").values_list("author", flat=True) ) ] if not is_sqlite_running else [ - Profile.objects.chip_summarize(p) - for p in Profile.objects.filter( + profile + for profile in Profile.objects.filter( pk__in=civis.values_list("author", flat=True).distinct() ) ], diff --git a/project/threads/views.py b/project/threads/views.py index bdf5fd46..9aa2d2b3 100644 --- a/project/threads/views.py +++ b/project/threads/views.py @@ -3,17 +3,18 @@ from accounts.models import Profile from accounts.utils import get_account from categories.models import Category +from common.utils import check_database from core.custom_decorators import full_profile, login_required from django.http import HttpResponse from django.contrib.auth.mixins import LoginRequiredMixin from django.template.response import TemplateResponse -from django.views.generic import TemplateView from django.views.generic.detail import DetailView from django.views.decorators.csrf import csrf_exempt +from django.views.generic import TemplateView from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet -from threads.models import Civi, CiviImage, Thread +from threads.models import Activity, Civi, CiviImage, Thread from threads.permissions import IsOwnerOrReadOnly from threads.serializers import ( CiviImageSerializer, @@ -191,6 +192,82 @@ def civi2csv(request, thread_id): return response +is_sqlite_running = check_database("sqlite") + + +@login_required +@full_profile +def issue_thread(request, thread_id=None): + if not thread_id: + return HttpResponseRedirect("/404") + + Thread_filter = get_object_or_404(Thread, pk=thread_id) + c_qs = Civi.objects.filter(thread_id=thread_id).exclude(c_type="response") + c_scored = [c.dict_with_score(request.user.id) for c in c_qs] + civis = sorted(c_scored, key=lambda c: c["score"], reverse=True) + + # modify thread view count + Thread_filter.num_civis = len(civis) + Thread_filter.num_views = F("num_views") + 1 + Thread_filter.save() + Thread_filter.refresh_from_db() + + thread_wiki_data = { + "thread_id": thread_id, + "title": Thread_filter.title, + "summary": Thread_filter.summary, + "image": Thread_filter.image_url, + "author": { + "username": Thread_filter.author.username, + "profile_image": Thread_filter.author.profile.profile_image_url, + "first_name": Thread_filter.author.first_name, + "last_name": Thread_filter.author.last_name, + }, + "contributors": [ + profile + for profile in Profile.objects.filter( + pk__in=c_qs.distinct("author").values_list("author", flat=True) + ) + ] + if not is_sqlite_running + else [ + profile + for profile in Profile.objects.filter( + pk__in=c_qs.values_list("author", flat=True).distinct() + ) + ], + "category": { + "id": Thread_filter.category.id, + "name": Thread_filter.category.name, + }, + "categories": [{"id": c.id, "name": c.name} for c in Category.objects.all()], + "created": Thread_filter.created_date_str, + "num_civis": Thread_filter.num_civis, + "num_views": Thread_filter.num_views, + "user_votes": [ + { + "civi_id": act.civi.id, + "activity_type": act.activity_type, + "c_type": act.civi.c_type, + } + for act in Activity.objects.filter( + thread=Thread_filter.id, user=request.user.id + ) + ], + } + thread_body_data = { + "civis": civis, + } + + data = { + "thread_id": thread_id, + "is_draft": Thread_filter.is_draft, + "thread_wiki_data": json.dumps(thread_wiki_data), + "thread_body_data": json.dumps(thread_body_data), + } + return TemplateResponse(request, "thread.html", data) + + @login_required @full_profile def create_group(request): From 15211505659cc80eb0fabeffe7d5aa7d20e35d8a Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 18 Oct 2022 21:20:38 +0300 Subject: [PATCH 41/63] Restore file from main --- project/threads/views.py | 83 ++-------------------------------------- 1 file changed, 3 insertions(+), 80 deletions(-) diff --git a/project/threads/views.py b/project/threads/views.py index 9aa2d2b3..b01a57d4 100644 --- a/project/threads/views.py +++ b/project/threads/views.py @@ -3,18 +3,17 @@ from accounts.models import Profile from accounts.utils import get_account from categories.models import Category -from common.utils import check_database from core.custom_decorators import full_profile, login_required -from django.http import HttpResponse from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import HttpResponse from django.template.response import TemplateResponse -from django.views.generic.detail import DetailView from django.views.decorators.csrf import csrf_exempt from django.views.generic import TemplateView +from django.views.generic.detail import DetailView from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet -from threads.models import Activity, Civi, CiviImage, Thread +from threads.models import Civi, CiviImage, Thread from threads.permissions import IsOwnerOrReadOnly from threads.serializers import ( CiviImageSerializer, @@ -192,82 +191,6 @@ def civi2csv(request, thread_id): return response -is_sqlite_running = check_database("sqlite") - - -@login_required -@full_profile -def issue_thread(request, thread_id=None): - if not thread_id: - return HttpResponseRedirect("/404") - - Thread_filter = get_object_or_404(Thread, pk=thread_id) - c_qs = Civi.objects.filter(thread_id=thread_id).exclude(c_type="response") - c_scored = [c.dict_with_score(request.user.id) for c in c_qs] - civis = sorted(c_scored, key=lambda c: c["score"], reverse=True) - - # modify thread view count - Thread_filter.num_civis = len(civis) - Thread_filter.num_views = F("num_views") + 1 - Thread_filter.save() - Thread_filter.refresh_from_db() - - thread_wiki_data = { - "thread_id": thread_id, - "title": Thread_filter.title, - "summary": Thread_filter.summary, - "image": Thread_filter.image_url, - "author": { - "username": Thread_filter.author.username, - "profile_image": Thread_filter.author.profile.profile_image_url, - "first_name": Thread_filter.author.first_name, - "last_name": Thread_filter.author.last_name, - }, - "contributors": [ - profile - for profile in Profile.objects.filter( - pk__in=c_qs.distinct("author").values_list("author", flat=True) - ) - ] - if not is_sqlite_running - else [ - profile - for profile in Profile.objects.filter( - pk__in=c_qs.values_list("author", flat=True).distinct() - ) - ], - "category": { - "id": Thread_filter.category.id, - "name": Thread_filter.category.name, - }, - "categories": [{"id": c.id, "name": c.name} for c in Category.objects.all()], - "created": Thread_filter.created_date_str, - "num_civis": Thread_filter.num_civis, - "num_views": Thread_filter.num_views, - "user_votes": [ - { - "civi_id": act.civi.id, - "activity_type": act.activity_type, - "c_type": act.civi.c_type, - } - for act in Activity.objects.filter( - thread=Thread_filter.id, user=request.user.id - ) - ], - } - thread_body_data = { - "civis": civis, - } - - data = { - "thread_id": thread_id, - "is_draft": Thread_filter.is_draft, - "thread_wiki_data": json.dumps(thread_wiki_data), - "thread_body_data": json.dumps(thread_body_data), - } - return TemplateResponse(request, "thread.html", data) - - @login_required @full_profile def create_group(request): From 3ed578fbb1897dfcfca66fce038708d16f17613a Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 18 Oct 2022 21:27:27 +0300 Subject: [PATCH 42/63] Remove unneeded tests --- project/threads/tests/test_api.py | 281 ------------------------------ 1 file changed, 281 deletions(-) delete mode 100644 project/threads/tests/test_api.py diff --git a/project/threads/tests/test_api.py b/project/threads/tests/test_api.py deleted file mode 100644 index c03efe3f..00000000 --- a/project/threads/tests/test_api.py +++ /dev/null @@ -1,281 +0,0 @@ -import os -import json -from PIL import Image -from django.conf import settings -from django.core.files.temp import NamedTemporaryFile -from django.contrib.auth import get_user_model -from django.test import TestCase -from rest_framework.test import APIClient -from django.urls import reverse -from categories.models import Category -from threads.models import Civi, CiviImage, Thread - - -class BaseTestCase(TestCase): - """Base test class to set up test cases""" - - def setUp(self) -> None: - self.client = APIClient() - self.user = get_user_model().objects.create_user( - username="newuser", email="test@test.com", password="password123" - ) - self.user2 = get_user_model().objects.create_user( - username="newuser2", email="test2@test.com", password="password123" - ) - self.superuser = get_user_model().objects.create_superuser( - username="superuser", email="superuser@su.com", password="superpassword123" - ) - self.user.profile.following.add(self.superuser.profile) - self.user.profile.followers.add(self.superuser.profile, self.user2.profile) - self.category = Category.objects.create(name="NewCategory") - self.user.profile.categories.add(self.category) - self.thread = Thread.objects.create( - author=self.user, - title="Thread", - summary="summary", - category=self.category, - is_draft=False, - ) - self.draft_thread = Thread.objects.create( - author=self.user, - title="Draft Thread", - summary="draft_summary", - category=self.category, - ) - self.civi = Civi.objects.create( - author=self.user, thread=self.thread, title="Civi", body="body" - ) - - def tearDown(self) -> None: - self.client.logout() - - -class NewThreadTests(BaseTestCase): - """A class to test new_thread function""" - - def test_creation_of_a_new_thread(self): - """Whether a new thread is created as expected""" - - number_of_threads = self.user.thread_set.count() - thread_data = { - "title": "New Thread", - "summary": "Summary", - "category_id": self.category.id, - } - self.client.login(username="newuser", password="password123") - response = self.client.post(reverse("new thread"), data=thread_data) - content = json.loads(response.content) - self.assertEqual(self.user.thread_set.count(), number_of_threads + 1) - self.assertEqual(content.get("data"), "success") - self.assertEqual(content.get("thread_id"), self.user.thread_set.last().id) - - -class GetThreadTests(BaseTestCase): - """A class to test get_thread function""" - - def test_get_thread(self): - """Whether a thread is retrieved as expected""" - - self.client.login(username="newuser", password="password123") - response = self.client.get(reverse("get thread", args=[self.thread.id])) - content = json.loads(response.content) - self.assertEqual(response.status_code, 200) - self.assertEqual(content["title"], "Thread") - self.assertEqual(content["author"]["username"], "newuser") - self.assertEqual(content["category"]["name"], "NewCategory") - - -class CreateCiviTests(BaseTestCase): - """A class to test create_civi function""" - - def test_create_new_civi(self): - """Whether a new civi is created as expected""" - - new_civi_data = { - "thread_id": self.thread.pk, - "title": "New Civi", - "body": "New Body", - "c_type": self.category.name, - } - self.client.login(username="newuser", password="password123") - response = self.client.post(reverse("new civi"), data=new_civi_data) - content = json.loads(response.content) - self.assertEqual(content["data"]["thread_id"], self.thread.id) - self.assertEqual(content["data"]["title"], "New Civi") - self.assertEqual(content["data"]["body"], "New Body") - self.assertEqual(content["data"]["author"]["username"], self.user.username) - - -class EditThreadTests(BaseTestCase): - """A class to test edit_thread function""" - - def test_edit_thread(self): - """Whether a thread is edited as expected""" - - self.client.login(username="newuser", password="password123") - data = { - "thread_id": self.thread.id, - "title": "Edited Title", - "summary": "Edited summary", - } - response = self.client.post(reverse("edit thread"), data=data) - content = json.loads(response.content) - self.thread.refresh_from_db() - self.assertEqual(response.status_code, 200) - self.assertEqual(self.thread.title, "Edited Title") - self.assertEqual(self.thread.summary, "Edited summary") - self.assertEqual(int(content["data"]["thread_id"]), self.thread.id) - - def test_edit_nonexistent_thread(self): - """Whether editing a nonexistent thread gives 400 HTTP Status Code""" - - self.client.login(username="newuser", password="password123") - data = { - "thread_id": 12345, - "title": "Edited Title", - "summary": "Edited summary", - } - response = self.client.post(reverse("edit thread"), data=data) - content = json.loads(response.content) - self.assertEqual(response.status_code, 400) - self.assertIn("does not exist", content["error"]) - - def test_edit_thread_with_missing_id_field(self): - """Whether editing a thread with_missing id field gives 400 HTTP Status Code""" - - self.client.login(username="newuser", password="password123") - data = { - "title": "Edited Title", - "summary": "Edited summary", - } - response = self.client.post(reverse("edit thread"), data=data) - content = json.loads(response.content) - self.assertEqual(response.status_code, 400) - self.assertEqual(content["error"], "Invalid Thread Reference") - - -class EditCiviTests(BaseTestCase): - """A class to test edit_civi function""" - - def test_edit_civi(self): - """Whether a civi is edited as expected""" - - self.client.login(username="newuser", password="password123") - data = { - "civi_id": self.civi.id, - "title": "Edited Civi", - "body": "Edited body", - } - response = self.client.post(reverse("edit civi"), data=data) - content = json.loads(response.content) - self.civi.refresh_from_db() - self.assertEqual(response.status_code, 200) - self.assertEqual(self.civi.title, "Edited Civi") - self.assertEqual(self.civi.body, "Edited body") - self.assertEqual(content.get("thread_id"), 1) - self.assertEqual(content.get("title"), "Edited Civi") - - def test_edit_nonexistent_civi(self): - """Whether editing a nonexistent civi gives 400 HTTP Status Code""" - - self.client.login(username="newuser", password="password123") - data = { - "civi_id": 12345, - "title": "Edited Civi", - "body": "Edited body", - } - response = self.client.post(reverse("edit civi"), data=data) - content = json.loads(response.content) - self.assertEqual(response.status_code, 400) - self.assertIn("does not exist", content["error"]) - - -class DeleteCiviTests(BaseTestCase): - """A class to test delete_civi function""" - - def test_delete_civi(self): - """Whether a civi is deleted as expected""" - - number_of_civis = Civi.objects.count() - self.client.login(username="newuser", password="password123") - data = {"civi_id": self.civi.id} - response = self.client.post(reverse("delete civi"), data=data) - content = json.loads(response.content) - self.assertEqual(response.status_code, 200) - self.assertEqual(content.get("result"), "Success") - self.assertEqual(Civi.objects.count(), number_of_civis - 1) - - def test_only_civi_authors_have_right_to_delete(self): - """Whether only owner of a civi can delete the civi""" - - self.client.login(username="newuser2", password="password123") - data = {"civi_id": self.civi.id} - response = self.client.post(reverse("delete civi"), data=data) - content = json.loads(response.content) - self.assertEqual(response.status_code, 400) - self.assertEqual(content.get("error"), "No Edit Rights") - - -class UploadThreadImageTests(BaseTestCase): - """A class to test upload_thread_image function""" - - def setUp(self) -> None: - super(UploadThreadImageTests, self).setUp() - self.client.login(username="newuser", password="password123") - self.url = reverse("upload image") - self.image = Image.new("RGB", size=(5, 5), color=(0, 0, 0)) - self.file = NamedTemporaryFile(suffix=".jpg") - self.image.save(self.file) - - def tearDown(self) -> None: - """Delete test thread images from the file system""" - - for thread in Thread.objects.all(): - thread.image.delete() - - def test_upload_thread_image(self): - """Whether a thread image is uploaded as expected""" - - with open(self.file.name, "rb") as image_file: - data = {"thread_id": self.thread.id, "attachment_image": image_file} - response = self.client.post(self.url, data=data) - content = json.loads(response.content) - self.thread.refresh_from_db() - self.assertEqual(content.get("image"), self.thread.image_url) - self.assertNotEqual(self.thread.image_url, "/static/img/no_image_md.png") - - -class UploadCiviImageTests(BaseTestCase): - """A class to test upload_civi_image function""" - - def setUp(self) -> None: - super(UploadCiviImageTests, self).setUp() - self.client.login(username="newuser", password="password123") - self.url = reverse("upload images") - self.image = Image.new("RGB", size=(5, 5), color=(0, 0, 0)) - self.file = NamedTemporaryFile(suffix=".jpg") - self.image.save(self.file) - - def tearDown(self) -> None: - """Delete test civi images from the file system""" - - for civi_image in CiviImage.objects.all(): - image_path = os.path.join(settings.BASE_DIR, civi_image.image_url[1:]) - if os.path.isfile(image_path): - os.remove(image_path) - - def test_upload_civi_image(self): - """Whether a civi image is uploaded as expected""" - - with open(self.file.name, "rb") as image_file: - data = {"civi_id": self.civi.id, "attachment_image": image_file} - response = self.client.post(self.url, data=data) - content = json.loads(response.content) - self.thread.refresh_from_db() - self.assertEqual( - content.get("attachments")[0].get("image_url"), - self.civi.images.first().image_url, - ) - self.assertNotEqual( - self.civi.images.first().image_url, "/static/img/no_image_md.png" - ) From ca5d7d7743707fade8abc1020bc71b56669a2136 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 18 Oct 2022 21:35:27 +0300 Subject: [PATCH 43/63] Simplify following/followers relationship --- ...ofile_followers_alter_profile_following.py | 24 +++++++++++++++++++ project/accounts/models.py | 7 +----- 2 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 project/accounts/migrations/0008_remove_profile_followers_alter_profile_following.py diff --git a/project/accounts/migrations/0008_remove_profile_followers_alter_profile_following.py b/project/accounts/migrations/0008_remove_profile_followers_alter_profile_following.py new file mode 100644 index 00000000..b3967029 --- /dev/null +++ b/project/accounts/migrations/0008_remove_profile_followers_alter_profile_following.py @@ -0,0 +1,24 @@ +# Generated by Django 4.1.2 on 2022-10-18 18:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("accounts", "0007_auto_20211030_1148"), + ] + + operations = [ + migrations.RemoveField( + model_name="profile", + name="followers", + ), + migrations.AlterField( + model_name="profile", + name="following", + field=models.ManyToManyField( + related_name="followers", to="accounts.profile" + ), + ), + ] diff --git a/project/accounts/models.py b/project/accounts/models.py index 0adc1f58..f83c9743 100644 --- a/project/accounts/models.py +++ b/project/accounts/models.py @@ -52,12 +52,7 @@ class Profile(models.Model): ) tags = TaggableManager() - followers = models.ManyToManyField( - "self", related_name="follower", symmetrical=False - ) - following = models.ManyToManyField( - "self", related_name="followings", symmetrical=False - ) + following = models.ManyToManyField("self", related_name="followers") is_verified = models.BooleanField(default=False) full_profile = models.BooleanField(default=False) From 6eb852098920536a7d0be2edebd2a7d366c81b0d Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 18 Oct 2022 21:37:37 +0300 Subject: [PATCH 44/63] Remove full_profile field/checks --- .../0009_remove_profile_full_profile.py | 17 +++++++++++++++++ project/accounts/migrations/max_migration.txt | 2 +- project/accounts/models.py | 9 --------- project/accounts/permissions.py | 13 +------------ project/core/custom_decorators.py | 13 +------------ project/threads/views.py | 3 +-- 6 files changed, 21 insertions(+), 36 deletions(-) create mode 100644 project/accounts/migrations/0009_remove_profile_full_profile.py diff --git a/project/accounts/migrations/0009_remove_profile_full_profile.py b/project/accounts/migrations/0009_remove_profile_full_profile.py new file mode 100644 index 00000000..bb3fd511 --- /dev/null +++ b/project/accounts/migrations/0009_remove_profile_full_profile.py @@ -0,0 +1,17 @@ +# Generated by Django 4.1.2 on 2022-10-18 18:36 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("accounts", "0008_remove_profile_followers_alter_profile_following"), + ] + + operations = [ + migrations.RemoveField( + model_name="profile", + name="full_profile", + ), + ] diff --git a/project/accounts/migrations/max_migration.txt b/project/accounts/migrations/max_migration.txt index 106e635f..de926063 100644 --- a/project/accounts/migrations/max_migration.txt +++ b/project/accounts/migrations/max_migration.txt @@ -1 +1 @@ -0007_auto_20211030_1148 +0009_remove_profile_full_profile diff --git a/project/accounts/models.py b/project/accounts/models.py index f83c9743..ab52c66c 100644 --- a/project/accounts/models.py +++ b/project/accounts/models.py @@ -55,7 +55,6 @@ class Profile(models.Model): following = models.ManyToManyField("self", related_name="followers") is_verified = models.BooleanField(default=False) - full_profile = models.BooleanField(default=False) profile_image = models.ImageField( upload_to=PathAndRename("profile_uploads"), blank=True, null=True @@ -103,8 +102,6 @@ def save(self, *args, **kwargs): if self.profile_image: self.resize_profile_image() - self.full_profile = self.is_full_profile() - super(Profile, self).save(*args, **kwargs) def resize_profile_image(self): @@ -157,9 +154,3 @@ def resize_profile_image(self): thumb_image.tell(), None, ) - - def is_full_profile(self): - if self.first_name and self.last_name: - return True - else: - return False diff --git a/project/accounts/permissions.py b/project/accounts/permissions.py index 7fd8cdce..416c8d74 100644 --- a/project/accounts/permissions.py +++ b/project/accounts/permissions.py @@ -1,4 +1,4 @@ -from rest_framework.permissions import BasePermission, SAFE_METHODS +from rest_framework.permissions import SAFE_METHODS, BasePermission class IsProfileOwnerOrReadOnly(BasePermission): @@ -6,14 +6,3 @@ class IsProfileOwnerOrReadOnly(BasePermission): def has_object_permission(self, request, view, obj): return (request.method in SAFE_METHODS) or (obj.user == request.user) - - -class IsProfileOwnerOrDuringRegistrationOrReadOnly(IsProfileOwnerOrReadOnly): - """Check if request user is account owner or in the process of registering""" - - def has_object_permission(self, request, view, obj): - if obj.full_profile: - return super( - IsProfileOwnerOrDuringRegistrationOrReadOnly, self - ).has_object_permission(request, view, obj) - return True diff --git a/project/core/custom_decorators.py b/project/core/custom_decorators.py index 6d4845c2..739d5c9b 100644 --- a/project/core/custom_decorators.py +++ b/project/core/custom_decorators.py @@ -1,6 +1,6 @@ from functools import wraps + from django.http import HttpResponseBadRequest, HttpResponseRedirect -from accounts.models import Profile """ USAGE: @@ -25,17 +25,6 @@ def inner(request, *args, **kwargs): return decorator -def full_profile(func): - @wraps(func) - def inner(request, *args, **kwargs): - profile = Profile.objects.get(user=request.user) - if not profile.full_profile: - return HttpResponseRedirect("/setup") - return func(request, *args, **kwargs) - - return inner - - def login_required(func): @wraps(func) def inner(request, *args, **kwargs): diff --git a/project/threads/views.py b/project/threads/views.py index b01a57d4..bfbc292d 100644 --- a/project/threads/views.py +++ b/project/threads/views.py @@ -3,7 +3,7 @@ from accounts.models import Profile from accounts.utils import get_account from categories.models import Category -from core.custom_decorators import full_profile, login_required +from core.custom_decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpResponse from django.template.response import TemplateResponse @@ -192,7 +192,6 @@ def civi2csv(request, thread_id): @login_required -@full_profile def create_group(request): return TemplateResponse(request, "newgroup.html", {}) From fa5cc71d36b865d7fbe0864137b9a8499a5a6699 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 18 Oct 2022 21:58:35 +0300 Subject: [PATCH 45/63] Add model __str__ methods --- project/accounts/models.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/project/accounts/models.py b/project/accounts/models.py index ab52c66c..9219aa25 100644 --- a/project/accounts/models.py +++ b/project/accounts/models.py @@ -34,6 +34,9 @@ def upvoted_solutions(self): user=self.id, civi__c_type="solution", activity_type__contains="pos" ) + def __str__(self) -> str: + return self.username + # Image manipulation constants PROFILE_IMG_SIZE = (171, 171) @@ -63,6 +66,9 @@ class Profile(models.Model): upload_to=PathAndRename("profile_uploads"), blank=True, null=True ) + def __str__(self): + return f"{self.user.username} profile" + @property def full_name(self): """Returns the person's full name.""" From e9ffbf285ccc362c62cdce3019d85e972e84fb4b Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 18 Oct 2022 21:58:55 +0300 Subject: [PATCH 46/63] Register Profile model --- project/accounts/admin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/project/accounts/admin.py b/project/accounts/admin.py index 1d485528..46b1fad1 100644 --- a/project/accounts/admin.py +++ b/project/accounts/admin.py @@ -1,6 +1,8 @@ from django.contrib import admin -from .models import User +from .models import Profile, User # Register your models here. +admin.site.register(Profile) + admin.site.register(User) From ddad86d627660cfa06273945506e1e442566d658 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 18 Oct 2022 21:59:15 +0300 Subject: [PATCH 47/63] Move docstring --- project/core/custom_decorators.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/project/core/custom_decorators.py b/project/core/custom_decorators.py index 739d5c9b..9d1aff2b 100644 --- a/project/core/custom_decorators.py +++ b/project/core/custom_decorators.py @@ -2,15 +2,15 @@ from django.http import HttpResponseBadRequest, HttpResponseRedirect -""" -USAGE: - @require_post_params(params=['we', 'are', 'required']) - returns a bad request if all required parameters are not present in the POST -""" +def require_post_params(params): + """ + USAGE: + @require_post_params(params=['we', 'are', 'required']) + returns a bad request if all required parameters are not present in the POST + """ -def require_post_params(params): def decorator(func): @wraps(func) def inner(request, *args, **kwargs): From 5514fee2d8eda1c065ab4228a64a531da096954e Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 18 Oct 2022 22:09:16 +0300 Subject: [PATCH 48/63] Profile.following symmetrical=False --- project/accounts/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/project/accounts/models.py b/project/accounts/models.py index 9219aa25..4e22bbea 100644 --- a/project/accounts/models.py +++ b/project/accounts/models.py @@ -55,7 +55,9 @@ class Profile(models.Model): ) tags = TaggableManager() - following = models.ManyToManyField("self", related_name="followers") + following = models.ManyToManyField( + "self", related_name="followers", symmetrical=False + ) is_verified = models.BooleanField(default=False) From dd8197d795e840d6d4ea375ff0bdc0b5c2bd2cbd Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 18 Oct 2022 22:09:36 +0300 Subject: [PATCH 49/63] Add follow/unfollow views --- project/accounts/urls/urls.py | 10 ++++++++++ project/accounts/views.py | 25 ++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/project/accounts/urls/urls.py b/project/accounts/urls/urls.py index df73f6b1..a4247a95 100644 --- a/project/accounts/urls/urls.py +++ b/project/accounts/urls/urls.py @@ -3,6 +3,8 @@ PasswordResetConfirmView, PasswordResetDoneView, PasswordResetView, + ProfileFollow, + ProfileUnfollow, RegisterView, SettingsView, UserProfileView, @@ -21,6 +23,14 @@ path("register/", RegisterView.as_view(), name="accounts_register"), path("settings/", SettingsView.as_view(), name="accounts_settings"), path("profile//", UserProfileView.as_view(), name="profile"), + path( + "profile//follow", ProfileFollow.as_view(), name="profile-follow" + ), + path( + "profile//unfollow", + ProfileUnfollow.as_view(), + name="profile-unfollow", + ), path( "accounts/password_reset/", PasswordResetView.as_view(), diff --git a/project/accounts/views.py b/project/accounts/views.py index a045517c..d201b85c 100644 --- a/project/accounts/views.py +++ b/project/accounts/views.py @@ -13,13 +13,36 @@ from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.sites.shortcuts import get_current_site +from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse -from django.urls import reverse_lazy +from django.urls import reverse, reverse_lazy from django.views import View from django.views.generic.edit import FormView, UpdateView +class ProfileFollow(LoginRequiredMixin, View): + def get(self, request, *args, **kwargs): + following_profile = Profile.objects.get(user__username=kwargs["username"]) + + self.request.user.profile.following.add(following_profile) + + redirect_to = reverse("profile", kwargs={"username": kwargs["username"]}) + + return HttpResponseRedirect(redirect_to) + + +class ProfileUnfollow(LoginRequiredMixin, View): + def get(self, request, *args, **kwargs): + following_profile = Profile.objects.get(user__username=kwargs["username"]) + + self.request.user.profile.following.remove(following_profile) + + redirect_to = reverse("profile", kwargs={"username": kwargs["username"]}) + + return HttpResponseRedirect(redirect_to) + + class RegisterView(FormView): """ A form view that handles user registration. From 51c070ffd5f9c1bd0322148381132e75f2e29593 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Tue, 18 Oct 2022 22:19:19 +0300 Subject: [PATCH 50/63] Add follow/unfollow buttons --- project/accounts/templates/accounts/account.html | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/project/accounts/templates/accounts/account.html b/project/accounts/templates/accounts/account.html index da8a9368..e635de67 100644 --- a/project/accounts/templates/accounts/account.html +++ b/project/accounts/templates/accounts/account.html @@ -19,13 +19,17 @@ />
- {% if request.user.username != username|stringformat:"s" %} - {% endif %} + -{% endfor %} \ No newline at end of file +{% endfor %} From a887761788533ffd127194522d5a6ae3a7baa47e Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Thu, 20 Oct 2022 09:12:10 +0300 Subject: [PATCH 58/63] Move profile image settings to settings module --- project/accounts/models.py | 22 +++++++++++++--------- project/core/settings.py | 6 ++++++ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/project/accounts/models.py b/project/accounts/models.py index 4e22bbea..d12fd846 100644 --- a/project/accounts/models.py +++ b/project/accounts/models.py @@ -38,12 +38,6 @@ def __str__(self) -> str: return self.username -# Image manipulation constants -PROFILE_IMG_SIZE = (171, 171) -PROFILE_IMG_THUMB_SIZE = (40, 40) -WHITE_BG = (255, 255, 255) - - class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile") first_name = models.CharField(max_length=63, blank=False) @@ -125,12 +119,19 @@ def resize_profile_image(self): profile_image = Image.open(self.profile_image) # Resize image profile_image = ImageOps.fit( - profile_image, PROFILE_IMG_SIZE, Image.ANTIALIAS, centering=(0.5, 0.5) + profile_image, + settings.PROFILE_IMG["SIZE"], + Image.ANTIALIAS, + centering=(0.5, 0.5), ) # Convert to JPG image format with white background if profile_image.mode not in ("L", "RGB"): - white_bg_img = Image.new("RGB", PROFILE_IMG_SIZE, WHITE_BG) + white_bg_img = Image.new( + "RGB", + settings.PROFILE_IMG["SIZE"], + settings.PROFILE_IMG["WHITE_BG"], + ) white_bg_img.paste(profile_image, mask=profile_image.split()[3]) profile_image = white_bg_img @@ -149,7 +150,10 @@ def resize_profile_image(self): # Make a Thumbnail Image for the new resized image thumb_image = profile_image.copy() - thumb_image.thumbnail(PROFILE_IMG_THUMB_SIZE, resample=Image.ANTIALIAS) + thumb_image.thumbnail( + settings.PROFILE_IMG["THUMB_SIZE"], + resample=Image.ANTIALIAS, + ) tmp_thumb_file = io.BytesIO() thumb_image.save(tmp_thumb_file, "JPEG", quality=90) tmp_thumb_file.seek(0) diff --git a/project/core/settings.py b/project/core/settings.py index c7274c36..f05a14a1 100644 --- a/project/core/settings.py +++ b/project/core/settings.py @@ -109,6 +109,12 @@ STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" +PROFILE_IMG = { + "SIZE": (171, 171), + "THUMB_SIZE": (40, 40), + "WHITE_BG": (255, 255, 255), +} + # Use DATABASE_URL in production DATABASE_URL = os.getenv("DATABASE_URL") From 67ce9d0ccaf138241a384e5fef79c786e2b3aaf4 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Thu, 20 Oct 2022 09:28:04 +0300 Subject: [PATCH 59/63] Remove unused file --- project/accounts/permissions.py | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 project/accounts/permissions.py diff --git a/project/accounts/permissions.py b/project/accounts/permissions.py deleted file mode 100644 index 416c8d74..00000000 --- a/project/accounts/permissions.py +++ /dev/null @@ -1,8 +0,0 @@ -from rest_framework.permissions import SAFE_METHODS, BasePermission - - -class IsProfileOwnerOrReadOnly(BasePermission): - """Custom API permission to check if request user is the owner of the profile""" - - def has_object_permission(self, request, view, obj): - return (request.method in SAFE_METHODS) or (obj.user == request.user) From 829951359859e0b7278942d5fb80d0efb6f642b0 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Thu, 20 Oct 2022 09:44:37 +0300 Subject: [PATCH 60/63] Change view context to return profile --- project/accounts/templates/accounts/account.html | 16 ++++++++-------- project/accounts/views.py | 5 ++--- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/project/accounts/templates/accounts/account.html b/project/accounts/templates/accounts/account.html index e635de67..7e4eaf8a 100644 --- a/project/accounts/templates/accounts/account.html +++ b/project/accounts/templates/accounts/account.html @@ -15,33 +15,33 @@
- - {% if user.profile not in request.user.profile.following.all %} - {% else %} - {% endif %}
ABOUT ME
-
{{ user.profile.about_me }}
+
{{ profile.about_me }}
diff --git a/project/accounts/views.py b/project/accounts/views.py index d201b85c..e21001e0 100644 --- a/project/accounts/views.py +++ b/project/accounts/views.py @@ -126,14 +126,13 @@ class UserProfileView(LoginRequiredMixin, View): """A view that shows profile for authorized users""" def get(self, request, username=None): - user_model = get_user_model() - user = get_object_or_404(user_model, username=username) + profile = get_object_or_404(Profile, user__username=username) return TemplateResponse( request, "account.html", { - "user": user, + "profile": profile, }, ) From 2fa1c8985cacbb23dab58b2bf0a45a226fe7269b Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Thu, 20 Oct 2022 09:46:21 +0300 Subject: [PATCH 61/63] Prevent users from following themselves --- .../accounts/templates/accounts/account.html | 17 ++++++++++------- project/accounts/views.py | 16 ++++++++++++---- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/project/accounts/templates/accounts/account.html b/project/accounts/templates/accounts/account.html index 7e4eaf8a..e9c6602b 100644 --- a/project/accounts/templates/accounts/account.html +++ b/project/accounts/templates/accounts/account.html @@ -20,14 +20,17 @@ - {% if profile not in request.user.profile.following.all %} - - {% else %} - + {% else %} + + {% endif %} {% endif %}