aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar ks129 <[email protected]>2020-10-30 17:43:13 +0200
committerGravatar ks129 <[email protected]>2020-10-30 17:43:13 +0200
commit3fa3854eb192466ca59acb577bfe135b53c2da11 (patch)
treeaf5aa98d8d521f21f25c45528c89941ac23e77aa
parentCover fetching article GitHub information with tests (diff)
parentMerge pull request #395 from ks129/resources-home (diff)
Merge remote-tracking branch 'up/dewikification' into guides-app
-rw-r--r--.mdlrc1
-rw-r--r--Pipfile1
-rw-r--r--Pipfile.lock221
-rw-r--r--pydis_site/__init__.py4
-rw-r--r--pydis_site/apps/home/__init__.py1
-rw-r--r--pydis_site/apps/home/apps.py38
-rw-r--r--pydis_site/apps/home/forms/account_deletion.py10
-rw-r--r--pydis_site/apps/home/signals.py314
-rw-r--r--pydis_site/apps/home/tests/test_signal_listener.py458
-rw-r--r--pydis_site/apps/home/tests/test_views.py213
-rw-r--r--pydis_site/apps/home/urls.py31
-rw-r--r--pydis_site/apps/home/views/__init__.py3
-rw-r--r--pydis_site/apps/home/views/account/__init__.py4
-rw-r--r--pydis_site/apps/home/views/account/delete.py37
-rw-r--r--pydis_site/apps/home/views/account/settings.py59
-rw-r--r--pydis_site/apps/resources/__init__.py (renamed from pydis_site/apps/home/forms/__init__.py)0
-rw-r--r--pydis_site/apps/resources/apps.py7
-rw-r--r--pydis_site/apps/resources/migrations/__init__.py (renamed from pydis_site/tests/__init__.py)0
-rw-r--r--pydis_site/apps/resources/tests/__init__.py0
-rw-r--r--pydis_site/apps/resources/tests/test_views.py10
-rw-r--r--pydis_site/apps/resources/urls.py8
-rw-r--r--pydis_site/apps/resources/views/__init__.py3
-rw-r--r--pydis_site/apps/resources/views/resources.py7
-rw-r--r--pydis_site/apps/staff/admin.py6
-rw-r--r--pydis_site/apps/staff/migrations/0003_delete_rolemapping.py16
-rw-r--r--pydis_site/apps/staff/models/__init__.py3
-rw-r--r--pydis_site/apps/staff/models/role_mapping.py31
-rw-r--r--pydis_site/settings.py41
-rw-r--r--pydis_site/static/css/base/notification.css99
-rw-r--r--pydis_site/static/css/resources/resources.css29
-rw-r--r--pydis_site/static/js/base/modal.js100
-rw-r--r--pydis_site/templates/base/base.html13
-rw-r--r--pydis_site/templates/base/navbar.html49
-rw-r--r--pydis_site/templates/home/account/delete.html47
-rw-r--r--pydis_site/templates/home/account/settings.html136
-rw-r--r--pydis_site/templates/resources/resources.html90
-rw-r--r--pydis_site/tests/test_utils_account.py139
-rw-r--r--pydis_site/utils/account.py79
-rw-r--r--pydis_site/utils/views.py25
39 files changed, 305 insertions, 2028 deletions
diff --git a/.mdlrc b/.mdlrc
deleted file mode 100644
index 0c02cde4..00000000
--- a/.mdlrc
+++ /dev/null
@@ -1 +0,0 @@
-rules '~MD024'
diff --git a/Pipfile b/Pipfile
index 850c6b44..dae75ea0 100644
--- a/Pipfile
+++ b/Pipfile
@@ -16,7 +16,6 @@ whitenoise = "~=5.0"
requests = "~=2.21"
pyyaml = "~=5.1"
pyuwsgi = {version = "~=2.0", sys_platform = "!='win32'"}
-django-allauth = "~=0.41"
sentry-sdk = "~=0.14"
gitpython = "~=3.1.7"
markdown2 = "~=2.3.9"
diff --git a/Pipfile.lock b/Pipfile.lock
index 7b642e4d..18b0d582 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "a564091c8be701ba148292d490babded217f2885c129c2dd829ece4134f1258b"
+ "sha256": "c0e53fd7b7c3d2fc62331078d4ba9301a7e848cd39ec8b41f0f7bc6d911a4d88"
},
"pipfile-spec": 6,
"requires": {
@@ -18,10 +18,11 @@
"default": {
"asgiref": {
"hashes": [
- "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a",
- "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed"
+ "sha256:a5098bc870b80e7b872bff60bb363c7f2c2c89078759f6c47b53ff8c525a152e",
+ "sha256:cd88907ecaec59d78e4ac00ea665b03e571cb37e3a0e37b3702af1a9e86c365a"
],
- "version": "==3.2.10"
+ "markers": "python_version >= '3.5'",
+ "version": "==3.3.0"
},
"certifi": {
"hashes": [
@@ -37,13 +38,6 @@
],
"version": "==3.0.4"
},
- "defusedxml": {
- "hashes": [
- "sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93",
- "sha256:f684034d135af4c6cbb949b8a4d2ed61634515257a67299e5f940fbaa34377f5"
- ],
- "version": "==0.6.0"
- },
"django": {
"hashes": [
"sha256:2d14be521c3ae24960e5e83d4575e156a8c479a75c935224b671b1c6e66eddaf",
@@ -52,13 +46,6 @@
"index": "pypi",
"version": "==3.0.10"
},
- "django-allauth": {
- "hashes": [
- "sha256:f17209410b7f87da0a84639fd79d3771b596a6d3fc1a8e48ce50dabc7f441d30"
- ],
- "index": "pypi",
- "version": "==0.42.0"
- },
"django-environ": {
"hashes": [
"sha256:6c9d87660142608f63ec7d5ce5564c49b603ea8ff25da595fd6098f6dc82afde",
@@ -111,21 +98,23 @@
"sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac",
"sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9"
],
+ "markers": "python_version >= '3.4'",
"version": "==4.0.5"
},
"gitpython": {
"hashes": [
- "sha256:138016d519bf4dd55b22c682c904ed2fd0235c3612b2f8f65ce218ff358deed8",
- "sha256:a03f728b49ce9597a6655793207c6ab0da55519368ff5961e4a74ae475b9fa8e"
+ "sha256:6eea89b655917b500437e9668e4a12eabdcf00229a0df1762aabd692ef9b746b",
+ "sha256:befa4d101f91bad1b632df4308ec64555db684c360bd7d2130b4807d49ce86b8"
],
"index": "pypi",
- "version": "==3.1.9"
+ "version": "==3.1.11"
},
"idna": {
"hashes": [
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.10"
},
"libsass": {
@@ -148,53 +137,47 @@
},
"markdown2": {
"hashes": [
- "sha256:89526090907ae5ece66d783c434b35c29ee500c1986309e306ce2346273ada6a",
- "sha256:e6b401ec80b75e76a6b3dbb2c8ade513156fa55fa6c30b9640a1abf6184a07c8"
+ "sha256:85956d8119fa6378156fef65545d66705a842819d2e1b50379a2b9d2aaa17cf0",
+ "sha256:fef148e5fd68d4532286c3e2943e9d2c076a8ad781b0a70a9d599a0ffe91652d"
],
"index": "pypi",
- "version": "==2.3.9"
- },
- "oauthlib": {
- "hashes": [
- "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889",
- "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"
- ],
- "version": "==3.1.0"
+ "version": "==2.3.10"
},
"psycopg2-binary": {
"hashes": [
- "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c",
- "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67",
+ "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52",
"sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0",
- "sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db",
+ "sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729",
+ "sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da",
"sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94",
- "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52",
- "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b",
- "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd",
- "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550",
- "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679",
- "sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77",
- "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2",
- "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77",
+ "sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c",
+ "sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25",
+ "sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1",
"sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2",
+ "sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83",
+ "sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71",
"sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd",
+ "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd",
"sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859",
- "sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1",
- "sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25",
- "sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152",
- "sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf",
- "sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f",
- "sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729",
- "sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71",
"sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66",
+ "sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77",
+ "sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5",
"sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4",
+ "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c",
+ "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b",
"sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449",
- "sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da",
- "sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a",
- "sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c",
- "sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb",
"sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4",
- "sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5"
+ "sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152",
+ "sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f",
+ "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77",
+ "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550",
+ "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2",
+ "sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf",
+ "sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb",
+ "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67",
+ "sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db",
+ "sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a",
+ "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679"
],
"index": "pypi",
"version": "==2.8.6"
@@ -207,13 +190,6 @@
"index": "pypi",
"version": "==2.8.1"
},
- "python3-openid": {
- "hashes": [
- "sha256:33fbf6928f401e0b790151ed2b5290b02545e8775f982485205a066f874aaeaf",
- "sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b"
- ],
- "version": "==3.2.0"
- },
"pytz": {
"hashes": [
"sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed",
@@ -265,26 +241,20 @@
"index": "pypi",
"version": "==2.24.0"
},
- "requests-oauthlib": {
- "hashes": [
- "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d",
- "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"
- ],
- "version": "==1.3.0"
- },
"sentry-sdk": {
"hashes": [
- "sha256:1d91a0059d2d8bb980bec169578035c2f2d4b93cd8a4fb5b85c81904d33e221a",
- "sha256:6222cf623e404c3e62b8e0e81c6db866ac2d12a663b7c1f7963350e3f397522a"
+ "sha256:0eea248408d36e8e7037c7b73827bea20b13a4375bf1719c406cae6fcbc094e3",
+ "sha256:5cf36eb6b1dc62d55f3c64289792cbaebc8ffa5a9da14474f49b46d20caa7fc8"
],
"index": "pypi",
- "version": "==0.18.0"
+ "version": "==0.19.1"
},
"six": {
"hashes": [
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.15.0"
},
"smmap": {
@@ -292,21 +262,24 @@
"sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4",
"sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==3.0.4"
},
"sqlparse": {
"hashes": [
- "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e",
- "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548"
+ "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
+ "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
],
- "version": "==0.3.1"
+ "markers": "python_version >= '3.5'",
+ "version": "==0.4.1"
},
"urllib3": {
"hashes": [
- "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a",
- "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"
+ "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2",
+ "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"
],
- "version": "==1.25.10"
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
+ "version": "==1.25.11"
},
"whitenoise": {
"hashes": [
@@ -330,6 +303,7 @@
"sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594",
"sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==20.2.0"
},
"bandit": {
@@ -344,6 +318,7 @@
"sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d",
"sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1"
],
+ "markers": "python_full_version >= '3.6.1'",
"version": "==3.2.0"
},
"coverage": {
@@ -482,22 +457,32 @@
"sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac",
"sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9"
],
+ "markers": "python_version >= '3.4'",
"version": "==4.0.5"
},
"gitpython": {
"hashes": [
- "sha256:138016d519bf4dd55b22c682c904ed2fd0235c3612b2f8f65ce218ff358deed8",
- "sha256:a03f728b49ce9597a6655793207c6ab0da55519368ff5961e4a74ae475b9fa8e"
+ "sha256:6eea89b655917b500437e9668e4a12eabdcf00229a0df1762aabd692ef9b746b",
+ "sha256:befa4d101f91bad1b632df4308ec64555db684c360bd7d2130b4807d49ce86b8"
],
"index": "pypi",
- "version": "==3.1.9"
+ "version": "==3.1.11"
},
"identify": {
"hashes": [
- "sha256:7c22c384a2c9b32c5cc891d13f923f6b2653aa83e2d75d8f79be240d6c86c4f4",
- "sha256:da683bfb7669fa749fc7731f378229e2dbf29a1d1337cbde04106f02236eb29d"
+ "sha256:3139bf72d81dfd785b0a464e2776bd59bdc725b4cc10e6cf46b56a0db931c82e",
+ "sha256:969d844b7a85d32a5f9ac4e163df6e846d73c87c8b75847494ee8f4bd2186421"
],
- "version": "==1.5.5"
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+ "version": "==1.5.6"
+ },
+ "importlib-metadata": {
+ "hashes": [
+ "sha256:77a540690e24b0305878c37ffd421785a6f7e53c8b5720d211b211de8d0e95da",
+ "sha256:cefa1a2f919b866c5beb7c9f7b0ebb4061f30a8a9bf16d609b000e2dfaceb9c3"
+ ],
+ "markers": "python_version < '3.8'",
+ "version": "==2.0.0"
},
"mccabe": {
"hashes": [
@@ -516,10 +501,11 @@
},
"pbr": {
"hashes": [
- "sha256:14bfd98f51c78a3dd22a1ef45cf194ad79eee4a19e8e1a0d5c7f8e81ffe182ea",
- "sha256:5adc0f9fc64319d8df5ca1e4e06eea674c26b80e6f00c530b18ce6a6592ead15"
+ "sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9",
+ "sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00"
],
- "version": "==5.5.0"
+ "markers": "python_version >= '2.6'",
+ "version": "==5.5.1"
},
"pep8-naming": {
"hashes": [
@@ -531,17 +517,18 @@
},
"pre-commit": {
"hashes": [
- "sha256:810aef2a2ba4f31eed1941fc270e72696a1ad5590b9751839c90807d0fff6b9a",
- "sha256:c54fd3e574565fe128ecc5e7d2f91279772ddb03f8729645fa812fe809084a70"
+ "sha256:7eadaa7f4547a8a19b83230ce430ba81bbe4797bd41c8d7fb54b246164628d1f",
+ "sha256:8fb2037c404ef8c87125e72564f316cf2bc94fc9c1cb184b8352117de747e164"
],
"index": "pypi",
- "version": "==2.7.1"
+ "version": "==2.8.1"
},
"pycodestyle": {
"hashes": [
"sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367",
"sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.6.0"
},
"pydocstyle": {
@@ -549,6 +536,7 @@
"sha256:19b86fa8617ed916776a11cd8bc0197e5b9856d5433b777f51a3defe13075325",
"sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678"
],
+ "markers": "python_version >= '3.5'",
"version": "==5.1.1"
},
"pyflakes": {
@@ -556,6 +544,7 @@
"sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92",
"sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.2.0"
},
"pyyaml": {
@@ -580,6 +569,7 @@
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.15.0"
},
"smmap": {
@@ -587,6 +577,7 @@
"sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4",
"sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==3.0.4"
},
"snowballstemmer": {
@@ -601,6 +592,7 @@
"sha256:5e1ab03eaae06ef6ce23859402de785f08d97780ed774948ef16c4652c41bc62",
"sha256:f845868b3a3a77a2489d226568abe7328b5c2d4f6a011cc759dfa99144a521f0"
],
+ "markers": "python_version >= '3.6'",
"version": "==3.2.2"
},
"toml": {
@@ -610,6 +602,42 @@
],
"version": "==0.10.1"
},
+ "typed-ast": {
+ "hashes": [
+ "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355",
+ "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919",
+ "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d",
+ "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa",
+ "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652",
+ "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75",
+ "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c",
+ "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01",
+ "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d",
+ "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1",
+ "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907",
+ "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c",
+ "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3",
+ "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d",
+ "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b",
+ "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614",
+ "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c",
+ "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb",
+ "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395",
+ "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b",
+ "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41",
+ "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6",
+ "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34",
+ "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe",
+ "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072",
+ "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298",
+ "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91",
+ "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4",
+ "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f",
+ "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"
+ ],
+ "markers": "python_version < '3.8'",
+ "version": "==1.4.1"
+ },
"unittest-xml-reporting": {
"hashes": [
"sha256:7bf515ea8cb244255a25100cd29db611a73f8d3d0aaf672ed3266307e14cc1ca",
@@ -620,10 +648,19 @@
},
"virtualenv": {
"hashes": [
- "sha256:35ecdeb58cfc2147bb0706f7cdef69a8f34f1b81b6d49568174e277932908b8f",
- "sha256:a5e0d253fe138097c6559c906c528647254f437d1019af9d5a477b09bfa7300f"
+ "sha256:b0011228208944ce71052987437d3843e05690b2f23d1c7da4263fde104c97a2",
+ "sha256:b8d6110f493af256a40d65e29846c69340a947669eec8ce784fcf3dd3af28380"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+ "version": "==20.1.0"
+ },
+ "zipp": {
+ "hashes": [
+ "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108",
+ "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"
],
- "version": "==20.0.33"
+ "markers": "python_version >= '3.6'",
+ "version": "==3.4.0"
}
}
}
diff --git a/pydis_site/__init__.py b/pydis_site/__init__.py
index c15c59c8..e69de29b 100644
--- a/pydis_site/__init__.py
+++ b/pydis_site/__init__.py
@@ -1,4 +0,0 @@
-# Empty list of validators for Allauth to ponder over. This is referred to in settings.py
-# by a string because Allauth won't let us just give it a list _there_, we have to point
-# at a list _somewhere else_ instead.
-VALIDATORS = []
diff --git a/pydis_site/apps/home/__init__.py b/pydis_site/apps/home/__init__.py
index ecfab449..e69de29b 100644
--- a/pydis_site/apps/home/__init__.py
+++ b/pydis_site/apps/home/__init__.py
@@ -1 +0,0 @@
-default_app_config = "pydis_site.apps.home.apps.HomeConfig"
diff --git a/pydis_site/apps/home/apps.py b/pydis_site/apps/home/apps.py
deleted file mode 100644
index 55a393a9..00000000
--- a/pydis_site/apps/home/apps.py
+++ /dev/null
@@ -1,38 +0,0 @@
-from typing import Any, Dict
-
-from django.apps import AppConfig
-
-
-class HomeConfig(AppConfig):
- """Django AppConfig for the home app."""
-
- name = 'pydis_site.apps.home'
- signal_listener = None
-
- def ready(self) -> None:
- """Run when the app has been loaded and is ready to serve requests."""
- from pydis_site.apps.home.signals import AllauthSignalListener
-
- self.signal_listener = AllauthSignalListener()
- self.patch_allauth()
-
- def patch_allauth(self) -> None:
- """Monkey-patches Allauth classes so we never collect email addresses."""
- # Imported here because we can't import it before our apps are loaded up
- from allauth.socialaccount.providers.base import Provider
-
- def extract_extra_data(_: Provider, data: Dict[str, Any]) -> Dict[str, Any]:
- """
- Extracts extra data for a SocialAccount provided by Allauth.
-
- This is our version of this function that strips the email address from incoming extra
- data. We do this so that we never have to store it.
-
- This is monkey-patched because most OAuth providers - or at least the ones we care
- about - all use the function from the base Provider class. This means we don't have
- to make a new Django app for each one we want to work with.
- """
- data["email"] = ""
- return data
-
- Provider.extract_extra_data = extract_extra_data
diff --git a/pydis_site/apps/home/forms/account_deletion.py b/pydis_site/apps/home/forms/account_deletion.py
deleted file mode 100644
index eec70bea..00000000
--- a/pydis_site/apps/home/forms/account_deletion.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from django.forms import CharField, Form
-
-
-class AccountDeletionForm(Form):
- """Account deletion form, to collect username for confirmation of removal."""
-
- username = CharField(
- label="Username",
- required=True
- )
diff --git a/pydis_site/apps/home/signals.py b/pydis_site/apps/home/signals.py
deleted file mode 100644
index 8af48c15..00000000
--- a/pydis_site/apps/home/signals.py
+++ /dev/null
@@ -1,314 +0,0 @@
-from contextlib import suppress
-from typing import List, Optional, Type
-
-from allauth.account.signals import user_logged_in
-from allauth.socialaccount.models import SocialAccount, SocialLogin
-from allauth.socialaccount.providers.base import Provider
-from allauth.socialaccount.providers.discord.provider import DiscordProvider
-from allauth.socialaccount.signals import (
- pre_social_login, social_account_added, social_account_removed,
- social_account_updated)
-from django.contrib.auth.models import Group, User as DjangoUser
-from django.db.models.signals import post_delete, post_save, pre_save
-
-from pydis_site.apps.api.models import User as DiscordUser
-from pydis_site.apps.staff.models import RoleMapping
-
-
-class AllauthSignalListener:
- """
- Listens to and processes events via the Django Signals system.
-
- Django Signals is basically an event dispatcher. It consists of Signals (which are the events)
- and Receivers, which listen for and handle those events. Signals are triggered by Senders,
- which are essentially just any class at all, and Receivers can filter the Signals they listen
- for by choosing a Sender, if required.
-
- Signals themselves define a set of arguments that they will provide to Receivers when the
- Signal is sent. They are always keyword arguments, and Django recommends that all Receiver
- functions accept them as `**kwargs` (and will supposedly error if you don't do this),
- supposedly because Signals can change in the future and your receivers should still work.
-
- Signals do provide a list of their arguments when they're initially constructed, but this
- is purely for documentation purposes only and Django does not enforce it.
-
- The Django Signals docs are here: https://docs.djangoproject.com/en/2.2/topics/signals/
- """
-
- def __init__(self):
- post_save.connect(self.user_model_updated, sender=DiscordUser)
-
- post_delete.connect(self.mapping_model_deleted, sender=RoleMapping)
- pre_save.connect(self.mapping_model_updated, sender=RoleMapping)
-
- pre_social_login.connect(self.social_account_updated)
- social_account_added.connect(self.social_account_updated)
- social_account_updated.connect(self.social_account_updated)
- social_account_removed.connect(self.social_account_removed)
-
- user_logged_in.connect(self.user_logged_in)
-
- def user_logged_in(self, sender: Type[DjangoUser], **kwargs) -> None:
- """
- Processes Allauth login signals to ensure a user has the correct perms.
-
- This method tries to find a Discord SocialAccount for a user - this should always
- be the case, but the admin user likely won't have one, so we do check for it.
-
- After that, we try to find the user's stored Discord account details, provided by the
- bot on the server. Finally, we pass the relevant information over to the
- `_apply_groups()` method for final processing.
- """
- user: DjangoUser = kwargs["user"]
-
- try:
- account: SocialAccount = SocialAccount.objects.get(
- user=user, provider=DiscordProvider.id
- )
- except SocialAccount.DoesNotExist:
- return # User's never linked a Discord account
-
- try:
- discord_user: DiscordUser = DiscordUser.objects.get(id=int(account.uid))
- except DiscordUser.DoesNotExist:
- return
-
- self._apply_groups(discord_user, account)
-
- def social_account_updated(self, sender: Type[SocialLogin], **kwargs) -> None:
- """
- Processes Allauth social account update signals to ensure a user has the correct perms.
-
- In this case, a SocialLogin is provided that we can check against. We check that this
- is a Discord login in order to ensure that future OAuth logins using other providers
- don't break things.
-
- Like most of the other methods that handle signals, this method defers to the
- `_apply_groups()` method for final processing.
- """
- social_login: SocialLogin = kwargs["sociallogin"]
-
- account: SocialAccount = social_login.account
- provider: Provider = account.get_provider()
-
- if not isinstance(provider, DiscordProvider):
- return
-
- try:
- user: DiscordUser = DiscordUser.objects.get(id=int(account.uid))
- except DiscordUser.DoesNotExist:
- return
-
- self._apply_groups(user, account)
-
- def social_account_removed(self, sender: Type[SocialLogin], **kwargs) -> None:
- """
- Processes Allauth social account reomval signals to ensure a user has the correct perms.
-
- In this case, a SocialAccount is provided that we can check against. If this is a
- Discord OAuth being removed from the account, we want to ensure that the user loses
- their permissions groups as well.
-
- While this isn't a realistic scenario to reach in our current setup, I've provided it
- for the sake of covering any edge cases and ensuring that SocialAccounts can be removed
- from Django users in the future if required.
-
- Like most of the other methods that handle signals, this method defers to the
- `_apply_groups()` method for final processing.
- """
- account: SocialAccount = kwargs["socialaccount"]
- provider: Provider = account.get_provider()
-
- if not isinstance(provider, DiscordProvider):
- return
-
- try:
- user: DiscordUser = DiscordUser.objects.get(id=int(account.uid))
- except DiscordUser.DoesNotExist:
- return
-
- self._apply_groups(user, account, deletion=True)
-
- def mapping_model_deleted(self, sender: Type[RoleMapping], **kwargs) -> None:
- """
- Processes deletion signals from the RoleMapping model, removing perms from users.
-
- We need to do this to ensure that users aren't left with permissions groups that
- they shouldn't have assigned to them when a RoleMapping is deleted from the database,
- and to remove their staff status if they should no longer have it.
- """
- instance: RoleMapping = kwargs["instance"]
-
- for user in instance.group.user_set.all():
- # Firstly, remove their related user group
- user.groups.remove(instance.group)
-
- with suppress(SocialAccount.DoesNotExist, DiscordUser.DoesNotExist):
- # If we get either exception, then the user could not have been assigned staff
- # with our system in the first place.
-
- social_account = SocialAccount.objects.get(user=user, provider=DiscordProvider.id)
- discord_user = DiscordUser.objects.get(id=int(social_account.uid))
-
- mappings = RoleMapping.objects.filter(role__id__in=discord_user.roles).all()
- is_staff = any(m.is_staff for m in mappings)
-
- if user.is_staff != is_staff:
- user.is_staff = is_staff
- user.save(update_fields=("is_staff", ))
-
- def mapping_model_updated(self, sender: Type[RoleMapping], **kwargs) -> None:
- """
- Processes update signals from the RoleMapping model.
-
- This method is in charge of figuring out what changed when a RoleMapping is updated
- (via the Django admin or otherwise). It operates based on what was changed, and can
- handle changes to both the role and permissions group assigned to it.
- """
- instance: RoleMapping = kwargs["instance"]
- raw: bool = kwargs["raw"]
-
- if raw:
- # Fixtures are being loaded, so don't touch anything
- return
-
- old_instance: Optional[RoleMapping] = None
-
- if instance.id is not None:
- # We don't try to catch DoesNotExist here because we can't test for it,
- # it should never happen (unless we have a bad DB failure) but I'm still
- # kind of antsy about not having the extra security here.
-
- old_instance = RoleMapping.objects.get(id=instance.id)
-
- if old_instance:
- self.mapping_model_deleted(RoleMapping, instance=old_instance)
-
- accounts = SocialAccount.objects.filter(
- uid__in=(u.id for u in DiscordUser.objects.filter(roles__contains=[instance.role.id]))
- )
-
- for account in accounts:
- account.user.groups.add(instance.group)
-
- if instance.is_staff and not account.user.is_staff:
- account.user.is_staff = instance.is_staff
- account.user.save(update_fields=("is_staff", ))
- else:
- discord_user = DiscordUser.objects.get(id=int(account.uid))
-
- mappings = RoleMapping.objects.filter(
- role__id__in=discord_user.roles
- ).exclude(id=instance.id).all()
- is_staff = any(m.is_staff for m in mappings)
-
- if account.user.is_staff != is_staff:
- account.user.is_staff = is_staff
- account.user.save(update_fields=("is_staff",))
-
- def user_model_updated(self, sender: Type[DiscordUser], **kwargs) -> None:
- """
- Processes update signals from the Discord User model, assigning perms as required.
-
- When a user's roles are changed on the Discord server, this method will ensure that
- the user has only the permissions groups that they should have based on the RoleMappings
- that have been set up in the Django admin.
-
- Like some of the other signal handlers, this method ensures that a SocialAccount exists
- for this Discord User, and defers to `_apply_groups()` to do the heavy lifting of
- ensuring the permissions groups are correct.
- """
- instance: DiscordUser = kwargs["instance"]
- raw: bool = kwargs["raw"]
-
- # `update_fields` could be used for checking changes, but it's None here due to how the
- # model is saved without using that argument - so we can't use it.
-
- if raw:
- # Fixtures are being loaded, so don't touch anything
- return
-
- try:
- account: SocialAccount = SocialAccount.objects.get(
- uid=str(instance.id), provider=DiscordProvider.id
- )
- except SocialAccount.DoesNotExist:
- return # User has never logged in with Discord on the site
-
- self._apply_groups(instance, account)
-
- def _apply_groups(
- self, user: DiscordUser, account: SocialAccount, deletion: bool = False
- ) -> None:
- """
- Ensures that the correct permissions are set for a Django user based on the RoleMappings.
-
- This (private) method is designed to check a Discord User against a given SocialAccount,
- and makes sure that the Django user associated with the SocialAccount has the correct
- permissions groups.
-
- While it would be possible to get the Discord User object with just the SocialAccount
- object, the current approach results in less queries.
-
- The `deletion` parameter is used to signify that the user's SocialAccount is about
- to be removed, and so we should always remove all of their permissions groups. The
- same thing will happen if the user is no longer actually on the Discord server, as
- leaving the server does not currently remove their SocialAccount from the database.
- """
- mappings = RoleMapping.objects.all()
-
- try:
- current_groups: List[Group] = list(account.user.groups.all())
- except SocialAccount.user.RelatedObjectDoesNotExist:
- return # There's no user account yet, this will be handled by another receiver
-
- # Ensure that the username on this account is correct
- new_username = f"{user.name}#{user.discriminator}"
-
- if account.user.username != new_username:
- account.user.username = new_username
- account.user.first_name = new_username
-
- if not user.in_guild:
- deletion = True
-
- if deletion:
- # They've unlinked Discord or left the server, so we have to remove their groups
- # and their staff status
-
- if current_groups:
- # They do have groups, so let's remove them
- account.user.groups.remove(
- *(mapping.group for mapping in mappings)
- )
-
- if account.user.is_staff:
- # They're marked as a staff user and they shouldn't be, so let's fix that
- account.user.is_staff = False
- else:
- new_groups = []
- is_staff = False
-
- for role in user.roles:
- try:
- mapping = mappings.get(role__id=role)
- except RoleMapping.DoesNotExist:
- continue # No mapping exists
-
- new_groups.append(mapping.group)
-
- if mapping.is_staff:
- is_staff = True
-
- account.user.groups.add(
- *[group for group in new_groups if group not in current_groups]
- )
-
- account.user.groups.remove(
- *[mapping.group for mapping in mappings if mapping.group not in new_groups]
- )
-
- if account.user.is_staff != is_staff:
- account.user.is_staff = is_staff
-
- account.user.save()
diff --git a/pydis_site/apps/home/tests/test_signal_listener.py b/pydis_site/apps/home/tests/test_signal_listener.py
deleted file mode 100644
index d99d81a5..00000000
--- a/pydis_site/apps/home/tests/test_signal_listener.py
+++ /dev/null
@@ -1,458 +0,0 @@
-from unittest import mock
-
-from allauth.account.signals import user_logged_in
-from allauth.socialaccount.models import SocialAccount, SocialLogin
-from allauth.socialaccount.providers import registry
-from allauth.socialaccount.providers.discord.provider import DiscordProvider
-from allauth.socialaccount.providers.github.provider import GitHubProvider
-from allauth.socialaccount.signals import (
- pre_social_login, social_account_added, social_account_removed,
- social_account_updated)
-from django.contrib.auth.models import Group, User as DjangoUser
-from django.db.models.signals import post_save, pre_save
-from django.test import TestCase
-
-from pydis_site.apps.api.models import Role, User as DiscordUser
-from pydis_site.apps.home.signals import AllauthSignalListener
-from pydis_site.apps.staff.models import RoleMapping
-
-
-class SignalListenerTests(TestCase):
- @classmethod
- def setUpTestData(cls):
- """
- Executed when testing begins in order to set up database fixtures required for testing.
-
- This sets up quite a lot of stuff, in order to try to cover every eventuality while
- ensuring that everything works when every possible situation is in the database
- at the same time.
-
- That does unfortunately mean that half of this file is just test fixtures, but I couldn't
- think of a better way to do this.
- """
- # This needs to be registered so we can test the role linking logic with a user that
- # doesn't have a Discord account linked, but is logged in somehow with another account
- # type anyway. The logic this is testing was designed so that the system would be
- # robust enough to handle that case, but it's impossible to fully test (and therefore
- # to have coverage of) those lines without an extra provider, and GH was the second
- # provider it was built with in mind.
- registry.register(GitHubProvider)
-
- cls.admin_role = Role.objects.create(
- id=0,
- name="admin",
- colour=0,
- permissions=0,
- position=0
- )
-
- cls.moderator_role = Role.objects.create(
- id=1,
- name="moderator",
- colour=0,
- permissions=0,
- position=1
- )
-
- cls.unmapped_role = Role.objects.create(
- id=2,
- name="unmapped",
- colour=0,
- permissions=0,
- position=1
- )
-
- cls.admin_group = Group.objects.create(name="admin")
- cls.moderator_group = Group.objects.create(name="moderator")
-
- cls.admin_mapping = RoleMapping.objects.create(
- role=cls.admin_role,
- group=cls.admin_group,
- is_staff=True
- )
-
- cls.moderator_mapping = RoleMapping.objects.create(
- role=cls.moderator_role,
- group=cls.moderator_group,
- is_staff=False
- )
-
- cls.discord_user = DiscordUser.objects.create(
- id=0,
- name="user",
- discriminator=0,
- )
-
- cls.discord_unmapped = DiscordUser.objects.create(
- id=2,
- name="unmapped",
- discriminator=0,
- )
-
- cls.discord_unmapped.roles.append(cls.unmapped_role.id)
- cls.discord_unmapped.save()
-
- cls.discord_not_in_guild = DiscordUser.objects.create(
- id=3,
- name="not-in-guild",
- discriminator=0,
- in_guild=False
- )
-
- cls.discord_admin = DiscordUser.objects.create(
- id=1,
- name="admin",
- discriminator=0,
- )
-
- cls.discord_admin.roles = [cls.admin_role.id]
- cls.discord_admin.save()
-
- cls.discord_moderator = DiscordUser.objects.create(
- id=4,
- name="admin",
- discriminator=0,
- )
-
- cls.discord_moderator.roles = [cls.moderator_role.id]
- cls.discord_moderator.save()
-
- cls.django_user_discordless = DjangoUser.objects.create(username="no-discord")
- cls.django_user_never_joined = DjangoUser.objects.create(username="never-joined")
-
- cls.social_never_joined = SocialAccount.objects.create(
- user=cls.django_user_never_joined,
- provider=DiscordProvider.id,
- uid=5
- )
-
- cls.django_user = DjangoUser.objects.create(username="user")
-
- cls.social_user = SocialAccount.objects.create(
- user=cls.django_user,
- provider=DiscordProvider.id,
- uid=cls.discord_user.id
- )
-
- cls.social_user_github = SocialAccount.objects.create(
- user=cls.django_user,
- provider=GitHubProvider.id,
- uid=cls.discord_user.id
- )
-
- cls.social_unmapped = SocialAccount(
- # We instantiate it and don't put it in the DB. This is (surprisingly)
- # a realistic test case, so we need to check for it
-
- provider=DiscordProvider.id,
- uid=5,
- user_id=None # No relation exists at all
- )
-
- cls.django_admin = DjangoUser.objects.create(
- username="admin",
- is_staff=True,
- is_superuser=True
- )
-
- cls.social_admin = SocialAccount.objects.create(
- user=cls.django_admin,
- provider=DiscordProvider.id,
- uid=cls.discord_admin.id
- )
-
- cls.django_moderator = DjangoUser.objects.create(
- username="moderator",
- is_staff=False,
- is_superuser=False
- )
-
- cls.social_moderator = SocialAccount.objects.create(
- user=cls.django_moderator,
- provider=DiscordProvider.id,
- uid=cls.discord_moderator.id
- )
-
- def test_model_save(self):
- """Test signal handling for when Discord user model objects are saved to DB."""
- mock_obj = mock.Mock()
-
- with mock.patch.object(AllauthSignalListener, "_apply_groups", mock_obj):
- AllauthSignalListener()
-
- post_save.send(
- DiscordUser,
- instance=self.discord_user,
- raw=True,
- created=None, # Not realistic, but we don't use it
- using=None, # Again, we don't use it
- update_fields=False # Always false during integration testing
- )
-
- mock_obj.assert_not_called()
-
- post_save.send(
- DiscordUser,
- instance=self.discord_user,
- raw=False,
- created=None, # Not realistic, but we don't use it
- using=None, # Again, we don't use it
- update_fields=False # Always false during integration testing
- )
-
- mock_obj.assert_called_with(self.discord_user, self.social_user)
-
- def test_pre_social_login(self):
- """Test the pre-social-login Allauth signal handling."""
- mock_obj = mock.Mock()
-
- discord_login = SocialLogin(self.django_user, self.social_user)
- github_login = SocialLogin(self.django_user, self.social_user_github)
- unmapped_login = SocialLogin(self.django_user, self.social_unmapped)
-
- with mock.patch.object(AllauthSignalListener, "_apply_groups", mock_obj):
- AllauthSignalListener()
-
- # Don't attempt to apply groups if the user doesn't have a linked Discord account
- pre_social_login.send(SocialLogin, sociallogin=github_login)
- mock_obj.assert_not_called()
-
- # Don't attempt to apply groups if the user hasn't joined the Discord server
- pre_social_login.send(SocialLogin, sociallogin=unmapped_login)
- mock_obj.assert_not_called()
-
- # Attempt to apply groups if everything checks out
- pre_social_login.send(SocialLogin, sociallogin=discord_login)
- mock_obj.assert_called_with(self.discord_user, self.social_user)
-
- def test_social_added(self):
- """Test the social-account-added Allauth signal handling."""
- mock_obj = mock.Mock()
-
- discord_login = SocialLogin(self.django_user, self.social_user)
- github_login = SocialLogin(self.django_user, self.social_user_github)
- unmapped_login = SocialLogin(self.django_user, self.social_unmapped)
-
- with mock.patch.object(AllauthSignalListener, "_apply_groups", mock_obj):
- AllauthSignalListener()
-
- # Don't attempt to apply groups if the user doesn't have a linked Discord account
- social_account_added.send(SocialLogin, sociallogin=github_login)
- mock_obj.assert_not_called()
-
- # Don't attempt to apply groups if the user hasn't joined the Discord server
- social_account_added.send(SocialLogin, sociallogin=unmapped_login)
- mock_obj.assert_not_called()
-
- # Attempt to apply groups if everything checks out
- social_account_added.send(SocialLogin, sociallogin=discord_login)
- mock_obj.assert_called_with(self.discord_user, self.social_user)
-
- def test_social_updated(self):
- """Test the social-account-updated Allauth signal handling."""
- mock_obj = mock.Mock()
-
- discord_login = SocialLogin(self.django_user, self.social_user)
- github_login = SocialLogin(self.django_user, self.social_user_github)
- unmapped_login = SocialLogin(self.django_user, self.social_unmapped)
-
- with mock.patch.object(AllauthSignalListener, "_apply_groups", mock_obj):
- AllauthSignalListener()
-
- # Don't attempt to apply groups if the user doesn't have a linked Discord account
- social_account_updated.send(SocialLogin, sociallogin=github_login)
- mock_obj.assert_not_called()
-
- # Don't attempt to apply groups if the user hasn't joined the Discord server
- social_account_updated.send(SocialLogin, sociallogin=unmapped_login)
- mock_obj.assert_not_called()
-
- # Attempt to apply groups if everything checks out
- social_account_updated.send(SocialLogin, sociallogin=discord_login)
- mock_obj.assert_called_with(self.discord_user, self.social_user)
-
- def test_social_removed(self):
- """Test the social-account-removed Allauth signal handling."""
- mock_obj = mock.Mock()
-
- with mock.patch.object(AllauthSignalListener, "_apply_groups", mock_obj):
- AllauthSignalListener()
-
- # Don't attempt to remove groups if the user doesn't have a linked Discord account
- social_account_removed.send(SocialLogin, socialaccount=self.social_user_github)
- mock_obj.assert_not_called()
-
- # Don't attempt to remove groups if the social account doesn't map to a Django user
- social_account_removed.send(SocialLogin, socialaccount=self.social_unmapped)
- mock_obj.assert_not_called()
-
- # Attempt to remove groups if everything checks out
- social_account_removed.send(SocialLogin, socialaccount=self.social_user)
- mock_obj.assert_called_with(self.discord_user, self.social_user, deletion=True)
-
- def test_logged_in(self):
- """Test the user-logged-in Allauth signal handling."""
- mock_obj = mock.Mock()
-
- with mock.patch.object(AllauthSignalListener, "_apply_groups", mock_obj):
- AllauthSignalListener()
-
- # Don't attempt to apply groups if the user doesn't have a linked Discord account
- user_logged_in.send(DjangoUser, user=self.django_user_discordless)
- mock_obj.assert_not_called()
-
- # Don't attempt to apply groups if the user hasn't joined the Discord server
- user_logged_in.send(DjangoUser, user=self.django_user_never_joined)
- mock_obj.assert_not_called()
-
- # Attempt to apply groups if everything checks out
- user_logged_in.send(DjangoUser, user=self.django_user)
- mock_obj.assert_called_with(self.discord_user, self.social_user)
-
- def test_apply_groups_admin(self):
- """Test application of groups by role, relating to an admin user."""
- handler = AllauthSignalListener()
-
- self.assertEqual(self.django_user_discordless.groups.all().count(), 0)
-
- # Apply groups based on admin role being present on Discord
- handler._apply_groups(self.discord_admin, self.social_admin)
- self.assertTrue(self.admin_group in self.django_admin.groups.all())
-
- # Remove groups based on the user apparently leaving the server
- handler._apply_groups(self.discord_admin, self.social_admin, True)
- self.assertEqual(self.django_user_discordless.groups.all().count(), 0)
-
- # Apply the admin role again
- handler._apply_groups(self.discord_admin, self.social_admin)
-
- # Remove all of the roles from the user
- self.discord_admin.roles.clear()
-
- # Remove groups based on the user no longer having the admin role on Discord
- handler._apply_groups(self.discord_admin, self.social_admin)
- self.assertEqual(self.django_user_discordless.groups.all().count(), 0)
-
- self.discord_admin.roles.append(self.admin_role.id)
- self.discord_admin.save()
-
- def test_apply_groups_moderator(self):
- """Test application of groups by role, relating to a non-`is_staff` moderator user."""
- handler = AllauthSignalListener()
-
- self.assertEqual(self.django_user_discordless.groups.all().count(), 0)
-
- # Apply groups based on moderator role being present on Discord
- handler._apply_groups(self.discord_moderator, self.social_moderator)
- self.assertTrue(self.moderator_group in self.django_moderator.groups.all())
-
- # Remove groups based on the user apparently leaving the server
- handler._apply_groups(self.discord_moderator, self.social_moderator, True)
- self.assertEqual(self.django_user_discordless.groups.all().count(), 0)
-
- # Apply the moderator role again
- handler._apply_groups(self.discord_moderator, self.social_moderator)
-
- # Remove all of the roles from the user
- self.discord_moderator.roles.clear()
-
- # Remove groups based on the user no longer having the moderator role on Discord
- handler._apply_groups(self.discord_moderator, self.social_moderator)
- self.assertEqual(self.django_user_discordless.groups.all().count(), 0)
-
- self.discord_moderator.roles.append(self.moderator_role.id)
- self.discord_moderator.save()
-
- def test_apply_groups_other(self):
- """Test application of groups by role, relating to non-standard cases."""
- handler = AllauthSignalListener()
-
- self.assertEqual(self.django_user_discordless.groups.all().count(), 0)
-
- # No groups should be applied when there's no user account yet
- handler._apply_groups(self.discord_unmapped, self.social_unmapped)
- self.assertEqual(self.django_user_discordless.groups.all().count(), 0)
-
- # No groups should be applied when there are only unmapped roles to match
- handler._apply_groups(self.discord_unmapped, self.social_user)
- self.assertEqual(self.django_user.groups.all().count(), 0)
-
- # No groups should be applied when the user isn't in the guild
- handler._apply_groups(self.discord_not_in_guild, self.social_user)
- self.assertEqual(self.django_user.groups.all().count(), 0)
-
- def test_role_mapping_str(self):
- """Test that role mappings stringify correctly."""
- self.assertEqual(
- str(self.admin_mapping),
- f"@{self.admin_role.name} -> {self.admin_group.name}"
- )
-
- def test_role_mapping_changes(self):
- """Test that role mapping listeners work when changes are made."""
- # Set up (just for this test)
- self.django_moderator.groups.add(self.moderator_group)
- self.django_admin.groups.add(self.admin_group)
-
- self.assertEqual(self.django_moderator.groups.all().count(), 1)
- self.assertEqual(self.django_admin.groups.all().count(), 1)
-
- # Test is_staff changes
- self.admin_mapping.is_staff = False
- self.admin_mapping.save()
-
- self.assertFalse(self.django_moderator.is_staff)
- self.assertFalse(self.django_admin.is_staff)
-
- self.admin_mapping.is_staff = True
- self.admin_mapping.save()
-
- self.django_admin.refresh_from_db(fields=("is_staff", ))
- self.assertTrue(self.django_admin.is_staff)
-
- # Test mapping deletion
- self.admin_mapping.delete()
-
- self.django_admin.refresh_from_db(fields=("is_staff",))
- self.assertEqual(self.django_admin.groups.all().count(), 0)
- self.assertFalse(self.django_admin.is_staff)
-
- # Test mapping update
- self.moderator_mapping.group = self.admin_group
- self.moderator_mapping.save()
-
- self.assertEqual(self.django_moderator.groups.all().count(), 1)
- self.assertTrue(self.admin_group in self.django_moderator.groups.all())
-
- # Test mapping creation
- new_mapping = RoleMapping.objects.create(
- role=self.admin_role,
- group=self.moderator_group,
- is_staff=True
- )
-
- self.assertEqual(self.django_admin.groups.all().count(), 1)
- self.assertTrue(self.moderator_group in self.django_admin.groups.all())
-
- self.django_admin.refresh_from_db(fields=("is_staff",))
- self.assertTrue(self.django_admin.is_staff)
-
- new_mapping.delete()
-
- # Test mapping creation (without is_staff)
- new_mapping = RoleMapping.objects.create(
- role=self.admin_role,
- group=self.moderator_group,
- )
-
- self.assertEqual(self.django_admin.groups.all().count(), 1)
- self.assertTrue(self.moderator_group in self.django_admin.groups.all())
-
- self.django_admin.refresh_from_db(fields=("is_staff",))
- self.assertFalse(self.django_admin.is_staff)
-
- # Test that nothing happens when fixtures are loaded
- pre_save.send(RoleMapping, instance=new_mapping, raw=True)
-
- self.assertEqual(self.django_admin.groups.all().count(), 1)
- self.assertTrue(self.moderator_group in self.django_admin.groups.all())
diff --git a/pydis_site/apps/home/tests/test_views.py b/pydis_site/apps/home/tests/test_views.py
index 572317a7..bd1671b1 100644
--- a/pydis_site/apps/home/tests/test_views.py
+++ b/pydis_site/apps/home/tests/test_views.py
@@ -1,198 +1,5 @@
-from allauth.socialaccount.models import SocialAccount
-from django.contrib.auth.models import User
-from django.http import HttpResponseRedirect
from django.test import TestCase
-from django_hosts.resolvers import get_host, reverse, reverse_host
-
-
-def check_redirect_url(
- response: HttpResponseRedirect, reversed_url: str, strip_params=True
-) -> bool:
- """
- Check whether a given redirect response matches a specific reversed URL.
-
- Arguments:
- * `response`: The HttpResponseRedirect returned by the test client
- * `reversed_url`: The URL returned by `reverse()`
- * `strip_params`: Whether to strip URL parameters (following a "?") from the URL given in the
- `response` object
- """
- host = get_host(None)
- hostname = reverse_host(host)
-
- redirect_url = response.url
-
- if strip_params and "?" in redirect_url:
- redirect_url = redirect_url.split("?", 1)[0]
-
- result = reversed_url == f"//{hostname}{redirect_url}"
- return result
-
-
-class TestAccountDeleteView(TestCase):
- def setUp(self) -> None:
- """Create an authorized Django user for testing purposes."""
- self.user = User.objects.create(
- username="user#0000"
- )
-
- def test_redirect_when_logged_out(self):
- """Test that the user is redirected to the homepage when not logged in."""
- url = reverse("account_delete")
- resp = self.client.get(url)
- self.assertEqual(resp.status_code, 302)
- self.assertTrue(check_redirect_url(resp, reverse("home")))
-
- def test_get_when_logged_in(self):
- """Test that the view returns a HTTP 200 when the user is logged in."""
- url = reverse("account_delete")
-
- self.client.force_login(self.user)
- resp = self.client.get(url)
- self.client.logout()
-
- self.assertEqual(resp.status_code, 200)
-
- def test_post_invalid(self):
- """Test that the user is redirected when the form is filled out incorrectly."""
- url = reverse("account_delete")
-
- self.client.force_login(self.user)
-
- resp = self.client.post(url, {})
- self.assertEqual(resp.status_code, 302)
- self.assertTrue(check_redirect_url(resp, url))
-
- resp = self.client.post(url, {"username": "user"})
- self.assertEqual(resp.status_code, 302)
- self.assertTrue(check_redirect_url(resp, url))
-
- self.client.logout()
-
- def test_post_valid(self):
- """Test that the account is deleted when the form is filled out correctly.."""
- url = reverse("account_delete")
-
- self.client.force_login(self.user)
-
- resp = self.client.post(url, {"username": "user#0000"})
- self.assertEqual(resp.status_code, 302)
- self.assertTrue(check_redirect_url(resp, reverse("home")))
-
- with self.assertRaises(User.DoesNotExist):
- User.objects.get(username=self.user.username)
-
- self.client.logout()
-
-
-class TestAccountSettingsView(TestCase):
- def setUp(self) -> None:
- """Create an authorized Django user for testing purposes."""
- self.user = User.objects.create(
- username="user#0000"
- )
-
- self.user_unlinked = User.objects.create(
- username="user#9999"
- )
-
- self.user_unlinked_discord = User.objects.create(
- username="user#1234"
- )
-
- self.user_unlinked_github = User.objects.create(
- username="user#1111"
- )
-
- self.github_account = SocialAccount.objects.create(
- user=self.user,
- provider="github",
- uid="0"
- )
-
- self.discord_account = SocialAccount.objects.create(
- user=self.user,
- provider="discord",
- uid="0000"
- )
-
- self.github_account_secondary = SocialAccount.objects.create(
- user=self.user_unlinked_discord,
- provider="github",
- uid="1"
- )
-
- self.discord_account_secondary = SocialAccount.objects.create(
- user=self.user_unlinked_github,
- provider="discord",
- uid="1111"
- )
-
- def test_redirect_when_logged_out(self):
- """Check that the user is redirected to the homepage when not logged in."""
- url = reverse("account_settings")
- resp = self.client.get(url)
- self.assertEqual(resp.status_code, 302)
- self.assertTrue(check_redirect_url(resp, reverse("home")))
-
- def test_get_when_logged_in(self):
- """Test that the view returns a HTTP 200 when the user is logged in."""
- url = reverse("account_settings")
-
- self.client.force_login(self.user)
- resp = self.client.get(url)
- self.client.logout()
-
- self.assertEqual(resp.status_code, 200)
-
- self.client.force_login(self.user_unlinked)
- resp = self.client.get(url)
- self.client.logout()
-
- self.assertEqual(resp.status_code, 200)
-
- self.client.force_login(self.user_unlinked_discord)
- resp = self.client.get(url)
- self.client.logout()
-
- self.assertEqual(resp.status_code, 200)
-
- self.client.force_login(self.user_unlinked_github)
- resp = self.client.get(url)
- self.client.logout()
-
- self.assertEqual(resp.status_code, 200)
-
- def test_post_invalid(self):
- """Test the behaviour of invalid POST submissions."""
- url = reverse("account_settings")
-
- self.client.force_login(self.user_unlinked)
-
- resp = self.client.post(url, {"provider": "discord"})
- self.assertEqual(resp.status_code, 302)
- self.assertTrue(check_redirect_url(resp, reverse("home")))
-
- resp = self.client.post(url, {"provider": "github"})
- self.assertEqual(resp.status_code, 302)
- self.assertTrue(check_redirect_url(resp, reverse("home")))
-
- self.client.logout()
-
- def test_post_valid(self):
- """Ensure that GitHub is unlinked with a valid POST submission."""
- url = reverse("account_settings")
-
- self.client.force_login(self.user)
-
- resp = self.client.post(url, {"provider": "github"})
- self.assertEqual(resp.status_code, 302)
- self.assertTrue(check_redirect_url(resp, reverse("home")))
-
- with self.assertRaises(SocialAccount.DoesNotExist):
- SocialAccount.objects.get(user=self.user, provider="github")
-
- self.client.logout()
+from django_hosts.resolvers import reverse
class TestIndexReturns200(TestCase):
@@ -201,21 +8,3 @@ class TestIndexReturns200(TestCase):
url = reverse('home')
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
-
-
-class TestLoginCancelledReturns302(TestCase):
- def test_login_cancelled_returns_302(self):
- """Check that the login cancelled redirect returns a HTTP 302 response."""
- url = reverse('socialaccount_login_cancelled')
- resp = self.client.get(url)
- self.assertEqual(resp.status_code, 302)
- self.assertTrue(check_redirect_url(resp, reverse("home")))
-
-
-class TestLoginErrorReturns302(TestCase):
- def test_login_error_returns_302(self):
- """Check that the login error redirect returns a HTTP 302 response."""
- url = reverse('socialaccount_login_error')
- resp = self.client.get(url)
- self.assertEqual(resp.status_code, 302)
- self.assertTrue(check_redirect_url(resp, reverse("home")))
diff --git a/pydis_site/apps/home/urls.py b/pydis_site/apps/home/urls.py
index 51093f50..80a3c587 100644
--- a/pydis_site/apps/home/urls.py
+++ b/pydis_site/apps/home/urls.py
@@ -1,37 +1,12 @@
-from allauth.account.views import LogoutView
from django.contrib import admin
-from django.contrib.messages import ERROR
-from django.urls import include, path
+from django.urls import path
-from pydis_site.utils.views import MessageRedirectView
-from .views import AccountDeleteView, AccountSettingsView, HomeView
+from .views import HomeView
app_name = 'home'
urlpatterns = [
- # We do this twice because Allauth expects specific view names to exist
path('', HomeView.as_view(), name='home'),
- path('', HomeView.as_view(), name='socialaccount_connections'),
-
- path('accounts/', include('allauth.socialaccount.providers.discord.urls')),
- path('accounts/', include('allauth.socialaccount.providers.github.urls')),
-
- path(
- 'accounts/login/cancelled', MessageRedirectView.as_view(
- pattern_name="home", message="Login cancelled."
- ), name='socialaccount_login_cancelled'
- ),
- path(
- 'accounts/login/error', MessageRedirectView.as_view(
- pattern_name="home", message="Login encountered an unknown error, please try again.",
- message_level=ERROR
- ), name='socialaccount_login_error'
- ),
-
- path('accounts/settings', AccountSettingsView.as_view(), name="account_settings"),
- path('accounts/delete', AccountDeleteView.as_view(), name="account_delete"),
-
- path('logout', LogoutView.as_view(), name="logout"),
-
path('admin/', admin.site.urls),
+ path('resources/', include('pydis_site.apps.resources.urls')),
path('articles/', include('pydis_site.apps.content.urls', namespace='articles')),
]
diff --git a/pydis_site/apps/home/views/__init__.py b/pydis_site/apps/home/views/__init__.py
index 801fd398..971d73a3 100644
--- a/pydis_site/apps/home/views/__init__.py
+++ b/pydis_site/apps/home/views/__init__.py
@@ -1,4 +1,3 @@
-from .account import DeleteView as AccountDeleteView, SettingsView as AccountSettingsView
from .home import HomeView
-__all__ = ["AccountDeleteView", "AccountSettingsView", "HomeView"]
+__all__ = ["HomeView"]
diff --git a/pydis_site/apps/home/views/account/__init__.py b/pydis_site/apps/home/views/account/__init__.py
deleted file mode 100644
index 3b3250ea..00000000
--- a/pydis_site/apps/home/views/account/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from .delete import DeleteView
-from .settings import SettingsView
-
-__all__ = ["DeleteView", "SettingsView"]
diff --git a/pydis_site/apps/home/views/account/delete.py b/pydis_site/apps/home/views/account/delete.py
deleted file mode 100644
index 798b8a33..00000000
--- a/pydis_site/apps/home/views/account/delete.py
+++ /dev/null
@@ -1,37 +0,0 @@
-from django.contrib.auth.mixins import LoginRequiredMixin
-from django.contrib.messages import ERROR, INFO, add_message
-from django.http import HttpRequest, HttpResponse
-from django.shortcuts import redirect, render
-from django.urls import reverse
-from django.views import View
-
-from pydis_site.apps.home.forms.account_deletion import AccountDeletionForm
-
-
-class DeleteView(LoginRequiredMixin, View):
- """Account deletion view, for removing linked user accounts from the DB."""
-
- def __init__(self, *args, **kwargs):
- self.login_url = reverse("home")
- super().__init__(*args, **kwargs)
-
- def get(self, request: HttpRequest) -> HttpResponse:
- """HTTP GET: Return the view template."""
- return render(
- request, "home/account/delete.html",
- context={"form": AccountDeletionForm()}
- )
-
- def post(self, request: HttpRequest) -> HttpResponse:
- """HTTP POST: Process the deletion, as requested by the user."""
- form = AccountDeletionForm(request.POST)
-
- if not form.is_valid() or request.user.username != form.cleaned_data["username"]:
- add_message(request, ERROR, "Please enter your username exactly as shown.")
-
- return redirect(reverse("account_delete"))
-
- request.user.delete()
- add_message(request, INFO, "Your account has been deleted.")
-
- return redirect(reverse("home"))
diff --git a/pydis_site/apps/home/views/account/settings.py b/pydis_site/apps/home/views/account/settings.py
deleted file mode 100644
index 3a817dbc..00000000
--- a/pydis_site/apps/home/views/account/settings.py
+++ /dev/null
@@ -1,59 +0,0 @@
-from allauth.socialaccount.models import SocialAccount
-from allauth.socialaccount.providers import registry
-from django.contrib.auth.mixins import LoginRequiredMixin
-from django.contrib.messages import ERROR, INFO, add_message
-from django.http import HttpRequest, HttpResponse
-from django.shortcuts import redirect, render
-from django.urls import reverse
-from django.views import View
-
-
-class SettingsView(LoginRequiredMixin, View):
- """
- Account settings view, for managing and deleting user accounts and connections.
-
- This view actually renders a template with a bare modal, and is intended to be
- inserted into another template using JavaScript.
- """
-
- def __init__(self, *args, **kwargs):
- self.login_url = reverse("home")
- super().__init__(*args, **kwargs)
-
- def get(self, request: HttpRequest) -> HttpResponse:
- """HTTP GET: Return the view template."""
- context = {
- "groups": request.user.groups.all(),
-
- "discord": None,
- "github": None,
-
- "discord_provider": registry.provider_map.get("discord"),
- "github_provider": registry.provider_map.get("github"),
- }
-
- for account in SocialAccount.objects.filter(user=request.user).all():
- if account.provider == "discord":
- context["discord"] = account
-
- if account.provider == "github":
- context["github"] = account
-
- return render(request, "home/account/settings.html", context=context)
-
- def post(self, request: HttpRequest) -> HttpResponse:
- """HTTP POST: Process account disconnections."""
- provider = request.POST["provider"]
-
- if provider == "github":
- try:
- account = SocialAccount.objects.get(user=request.user, provider=provider)
- except SocialAccount.DoesNotExist:
- add_message(request, ERROR, "You do not have a GitHub account linked.")
- else:
- account.delete()
- add_message(request, INFO, "The social account has been disconnected.")
- else:
- add_message(request, ERROR, f"Unknown provider: {provider}")
-
- return redirect(reverse("home"))
diff --git a/pydis_site/apps/home/forms/__init__.py b/pydis_site/apps/resources/__init__.py
index e69de29b..e69de29b 100644
--- a/pydis_site/apps/home/forms/__init__.py
+++ b/pydis_site/apps/resources/__init__.py
diff --git a/pydis_site/apps/resources/apps.py b/pydis_site/apps/resources/apps.py
new file mode 100644
index 00000000..e0c235bd
--- /dev/null
+++ b/pydis_site/apps/resources/apps.py
@@ -0,0 +1,7 @@
+from django.apps import AppConfig
+
+
+class ResourcesConfig(AppConfig):
+ """AppConfig instance for Resources app."""
+
+ name = 'resources'
diff --git a/pydis_site/tests/__init__.py b/pydis_site/apps/resources/migrations/__init__.py
index e69de29b..e69de29b 100644
--- a/pydis_site/tests/__init__.py
+++ b/pydis_site/apps/resources/migrations/__init__.py
diff --git a/pydis_site/apps/resources/tests/__init__.py b/pydis_site/apps/resources/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/pydis_site/apps/resources/tests/__init__.py
diff --git a/pydis_site/apps/resources/tests/test_views.py b/pydis_site/apps/resources/tests/test_views.py
new file mode 100644
index 00000000..497e9bfe
--- /dev/null
+++ b/pydis_site/apps/resources/tests/test_views.py
@@ -0,0 +1,10 @@
+from django.test import TestCase
+from django_hosts import reverse
+
+
+class TestResourcesView(TestCase):
+ def test_resources_index_200(self):
+ """Check does index of resources app return 200 HTTP response."""
+ url = reverse("resources:index")
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
diff --git a/pydis_site/apps/resources/urls.py b/pydis_site/apps/resources/urls.py
new file mode 100644
index 00000000..c91e306e
--- /dev/null
+++ b/pydis_site/apps/resources/urls.py
@@ -0,0 +1,8 @@
+from django.urls import path
+
+from pydis_site.apps.resources import views
+
+app_name = "resources"
+urlpatterns = [
+ path("", views.ResourcesView.as_view(), name="index"),
+]
diff --git a/pydis_site/apps/resources/views/__init__.py b/pydis_site/apps/resources/views/__init__.py
new file mode 100644
index 00000000..f54118f2
--- /dev/null
+++ b/pydis_site/apps/resources/views/__init__.py
@@ -0,0 +1,3 @@
+from .resources import ResourcesView
+
+__all__ = ["ResourcesView"]
diff --git a/pydis_site/apps/resources/views/resources.py b/pydis_site/apps/resources/views/resources.py
new file mode 100644
index 00000000..e770954b
--- /dev/null
+++ b/pydis_site/apps/resources/views/resources.py
@@ -0,0 +1,7 @@
+from django.views.generic import TemplateView
+
+
+class ResourcesView(TemplateView):
+ """View for resources index page."""
+
+ template_name = "resources/resources.html"
diff --git a/pydis_site/apps/staff/admin.py b/pydis_site/apps/staff/admin.py
deleted file mode 100644
index 94cd83c5..00000000
--- a/pydis_site/apps/staff/admin.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from django.contrib import admin
-
-from .models import RoleMapping
-
-
-admin.site.register(RoleMapping)
diff --git a/pydis_site/apps/staff/migrations/0003_delete_rolemapping.py b/pydis_site/apps/staff/migrations/0003_delete_rolemapping.py
new file mode 100644
index 00000000..e9b6114e
--- /dev/null
+++ b/pydis_site/apps/staff/migrations/0003_delete_rolemapping.py
@@ -0,0 +1,16 @@
+# Generated by Django 3.0.9 on 2020-10-04 17:49
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('staff', '0002_add_is_staff_to_role_mappings'),
+ ]
+
+ operations = [
+ migrations.DeleteModel(
+ name='RoleMapping',
+ ),
+ ]
diff --git a/pydis_site/apps/staff/models/__init__.py b/pydis_site/apps/staff/models/__init__.py
deleted file mode 100644
index b49b6fd0..00000000
--- a/pydis_site/apps/staff/models/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from .role_mapping import RoleMapping
-
-__all__ = ["RoleMapping"]
diff --git a/pydis_site/apps/staff/models/role_mapping.py b/pydis_site/apps/staff/models/role_mapping.py
deleted file mode 100644
index 8a1fac2e..00000000
--- a/pydis_site/apps/staff/models/role_mapping.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from django.contrib.auth.models import Group
-from django.db import models
-
-from pydis_site.apps.api.models import Role
-
-
-class RoleMapping(models.Model):
- """A mapping between a Discord role and Django permissions group."""
-
- role = models.OneToOneField(
- Role,
- on_delete=models.CASCADE,
- help_text="The Discord role to use for this mapping.",
- unique=True, # Unique in order to simplify group assignment logic
- )
-
- group = models.OneToOneField(
- Group,
- on_delete=models.CASCADE,
- help_text="The Django permissions group to use for this mapping.",
- unique=True, # Unique in order to simplify group assignment logic
- )
-
- is_staff = models.BooleanField(
- help_text="Whether this role mapping relates to a Django staff group",
- default=False
- )
-
- def __str__(self):
- """Returns the mapping, for display purposes."""
- return f"@{self.role.name} -> {self.group.name}"
diff --git a/pydis_site/settings.py b/pydis_site/settings.py
index 5d8353fe..c5937bb6 100644
--- a/pydis_site/settings.py
+++ b/pydis_site/settings.py
@@ -16,7 +16,6 @@ import sys
import environ
import sentry_sdk
-from django.contrib.messages import constants as messages
from sentry_sdk.integrations.django import DjangoIntegration
from pydis_site.constants import GIT_SHA
@@ -87,6 +86,7 @@ INSTALLED_APPS = [
'pydis_site.apps.api',
'pydis_site.apps.home',
'pydis_site.apps.staff',
+ 'pydis_site.apps.resources',
'pydis_site.apps.content',
'django.contrib.admin',
@@ -97,13 +97,6 @@ INSTALLED_APPS = [
'django.contrib.sites',
'django.contrib.staticfiles',
- 'allauth',
- 'allauth.account',
- 'allauth.socialaccount',
-
- 'allauth.socialaccount.providers.discord',
- 'allauth.socialaccount.providers.github',
-
'django_hosts',
'django_filters',
'django_simple_bulma',
@@ -268,22 +261,10 @@ LOGGING = {
}
}
-# Django Messages framework config
-MESSAGE_TAGS = {
- messages.DEBUG: 'primary',
- messages.INFO: 'info',
- messages.SUCCESS: 'success',
- messages.WARNING: 'warning',
- messages.ERROR: 'danger',
-}
-
# Custom settings for django-simple-bulma
BULMA_SETTINGS = {
"variables": { # If you update these colours, please update the notification.css file
"primary": "#7289DA", # Discord blurple
-
- # "orange": "", # Apparently unused, but the default is fine
- # "yellow": "", # The default yellow looks pretty good
"green": "#32ac66", # Colour picked after Discord discussion
"turquoise": "#7289DA", # Blurple, because Bulma uses this regardless of `primary` above
"blue": "#2482c1", # Colour picked after Discord discussion
@@ -299,26 +280,6 @@ BULMA_SETTINGS = {
}
}
-# Django Allauth stuff
-AUTHENTICATION_BACKENDS = (
- # Needed to login by username in Django admin, regardless of `allauth`
- 'django.contrib.auth.backends.ModelBackend',
-
- # `allauth` specific authentication methods, such as login by e-mail
- 'allauth.account.auth_backends.AuthenticationBackend',
-)
-
-ACCOUNT_ADAPTER = "pydis_site.utils.account.AccountAdapter"
-ACCOUNT_EMAIL_REQUIRED = False # Undocumented allauth setting; don't require emails
-ACCOUNT_EMAIL_VERIFICATION = "none" # No verification required; we don't use emails for anything
-
-# We use this validator because Allauth won't let us actually supply a list with no validators
-# in it, and we can't just give it a lambda - that'd be too easy, I suppose.
-ACCOUNT_USERNAME_VALIDATORS = "pydis_site.VALIDATORS"
-
-LOGIN_REDIRECT_URL = "home"
-SOCIALACCOUNT_ADAPTER = "pydis_site.utils.account.SocialAccountAdapter"
-
# Information about site repository
SITE_REPOSITORY_OWNER = env("SITE_REPOSITORY_OWNER")
SITE_REPOSITORY_NAME = env("SITE_REPOSITORY_NAME")
diff --git a/pydis_site/static/css/base/notification.css b/pydis_site/static/css/base/notification.css
deleted file mode 100644
index b2824641..00000000
--- a/pydis_site/static/css/base/notification.css
+++ /dev/null
@@ -1,99 +0,0 @@
-/* On-page message styling */
-
-@keyframes message-slide-in {
- 0% {
- transform: translateX(100%);
- }
-
- 100% {
- transform: translateX(0);
- }
-}
-
-div.messages {
- animation: 0.5s ease-out 0s 1 message-slide-in;
- padding: 0.5rem;
- position: fixed;
- right: 0;
- top: 76px;
-
- z-index: 1000; /* On top of everything else */
-}
-
-/* Discord light theme inspired notifications */
-
-.messages .notification {
- background-color: #fdfdfd; /* Discord embed background */
- border: #eeeeee 1px solid; /* Discord embed border */
- border-left: #4f545c 4px solid; /* Discord default embed colour */
- color: #4a4a4a; /* Bulma default colour */
-}
-
-.messages .notification.is-primary {
- background-color: #fdfdfd;
- border-left-color: #7289DA;
- color: #4a4a4a; /* Bulma default colour */
-}
-
-.messages .notification.is-info {
- background-color: #fdfdfd;
- border-left-color: #1c8ad3; /* Bulma default colour */
- color: #4a4a4a; /* Bulma default colour */
-}
-
-.messages .notification.is-success {
- background-color: #fdfdfd;
- border-left-color: #21c65c;
- color: #4a4a4a; /* Bulma default colour */
-}
-
-.messages .notification.is-warning {
- background-color: #fdfdfd;
- border-left-color: #ffdd57; /* Bulma default colour */
- color: #4a4a4a; /* Bulma default colour */
-}
-
-.messages .notification.is-danger {
- background-color: #fdfdfd;
- border-left-color: #ff3860; /* Bulma default colour */
- color: #4a4a4a; /* Bulma default colour */
-}
-
-/* Discord dark theme inspired notifications */
-
-.messages .notification.is-dark {
- background-color: #33353C; /* Discord embed background */
- border: #36393f 1px solid; /* Discord embed border */
- border-left: #4f545c 4px solid; /* Discord default embed colour */
- color: #fff; /* Bulma default colour */
-}
-
-.messages .notification.is-dark.is-primary {
- background-color: #33353C;
- border-left-color: #7289DA;
- color: #fff; /* Bulma default colour */
-}
-
-.messages .notification.is-dark.is-info {
- background-color: #33353C;
- border-left-color: #1c8ad3; /* Bulma default colour */
- color: #fff; /* Bulma default colour */
-}
-
-.messages .notification.is-dark.is-success {
- background-color: #33353C;
- border-left-color: #21c65c;
- color: #fff; /* Bulma default colour */
-}
-
-.messages .notification.is-dark.is-warning {
- background-color: #33353C;
- border-left-color: #ffdd57; /* Bulma default colour */
- color: #fff; /* Bulma default colour */
-}
-
-.messages .notification.is-dark.is-danger {
- background-color: #33353C;
- border-left-color: #ff3860; /* Bulma default colour */
- color: #fff; /* Bulma default colour */
-}
diff --git a/pydis_site/static/css/resources/resources.css b/pydis_site/static/css/resources/resources.css
new file mode 100644
index 00000000..cf4cb472
--- /dev/null
+++ b/pydis_site/static/css/resources/resources.css
@@ -0,0 +1,29 @@
+.box, .tile.is-parent {
+ transition: 0.1s ease-out;
+}
+.box {
+ min-height: 15vh;
+}
+.tile.is-parent:hover .box {
+ box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23);
+}
+.tile.is-parent:hover {
+ padding: 0.65rem 0.85rem 0.85rem 0.65rem;
+ filter: saturate(1.1) brightness(1.1);
+}
+
+#readingBlock {
+ background-image: linear-gradient(141deg, #911eb4 0%, #b631de 71%, #cf4bf7 100%);
+}
+
+#interactiveBlock {
+ background-image: linear-gradient(141deg, #d05600 0%, #da722a 71%, #e68846 100%);
+}
+
+#communitiesBlock {
+ background-image: linear-gradient(141deg, #3b756f 0%, #3a847c 71%, #41948b 100%);
+}
+
+#podcastsBlock {
+ background-image: linear-gradient(141deg, #232382 0%, #30309c 71%, #4343ad 100%);
+}
diff --git a/pydis_site/static/js/base/modal.js b/pydis_site/static/js/base/modal.js
deleted file mode 100644
index eccc8845..00000000
--- a/pydis_site/static/js/base/modal.js
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- modal.js: A simple way to wire up Bulma modals.
-
- This library is intended to be used with Bulma's modals, as described in the
- official Bulma documentation. It's based on the JavaScript that Bulma
- themselves use for this purpose on the modals documentation page.
-
- Note that, just like that piece of JavaScript, this library assumes that
- you will only ever want to have one modal open at once.
- */
-
-"use strict";
-
-// Event handler for the "esc" key, for closing modals.
-
-document.addEventListener("keydown", (event) => {
- const e = event || window.event;
-
- if (e.code === "Escape" || e.keyCode === 27) {
- closeModals();
- }
-});
-
-// An array of all the modal buttons we've already set up
-
-const modal_buttons = [];
-
-// Public API functions
-
-function setupModal(target) {
- // Set up a modal's events, given a DOM element. This can be
- // used later in order to set up a modal that was added after
- // this library has been run.
-
- // We need to collect a bunch of elements to work with
- const modal_background = Array.from(target.getElementsByClassName("modal-background"));
- const modal_close = Array.from(target.getElementsByClassName("modal-close"));
-
- const modal_head = Array.from(target.getElementsByClassName("modal-card-head"));
- const modal_foot = Array.from(target.getElementsByClassName("modal-card-foot"));
-
- const modal_delete = [];
- const modal_button = [];
-
- modal_head.forEach((element) => modal_delete.concat(Array.from(element.getElementsByClassName("delete"))));
- modal_foot.forEach((element) => modal_button.concat(Array.from(element.getElementsByClassName("button"))));
-
- // Collect all the elements that can be used to close modals
- const modal_closers = modal_background.concat(modal_close).concat(modal_delete).concat(modal_button);
-
- // Assign click events for closing modals
- modal_closers.forEach((element) => {
- element.addEventListener("click", () => {
- closeModals();
- });
- });
-
- setupOpeningButtons();
-}
-
-function setupOpeningButtons() {
- // Wire up all the opening buttons, avoiding buttons we've already wired up.
- const modal_opening_buttons = Array.from(document.getElementsByClassName("modal-button"));
-
- modal_opening_buttons.forEach((element) => {
- if (!modal_buttons.includes(element)) {
- element.addEventListener("click", () => {
- openModal(element.dataset.target);
- });
-
- modal_buttons.push(element);
- }
- });
-}
-
-function openModal(target) {
- // Open a modal, given a string ID
- const element = document.getElementById(target);
-
- document.documentElement.classList.add("is-clipped");
- element.classList.add("is-active");
-}
-
-function closeModals() {
- // Close all open modals
- const modals = Array.from(document.getElementsByClassName("modal"));
- document.documentElement.classList.remove("is-clipped");
-
- modals.forEach((element) => {
- element.classList.remove("is-active");
- });
-}
-
-(function () {
- // Set up all the modals currently on the page
- const modals = Array.from(document.getElementsByClassName("modal"));
-
- modals.forEach((modal) => setupModal(modal));
- setupOpeningButtons();
-}());
diff --git a/pydis_site/templates/base/base.html b/pydis_site/templates/base/base.html
index 905d408c..6fc0c6bb 100644
--- a/pydis_site/templates/base/base.html
+++ b/pydis_site/templates/base/base.html
@@ -30,7 +30,6 @@
<script src="{% static "js/base/modal.js" %}"></script>
<link rel="stylesheet" href="{% static "css/base/base.css" %}">
- <link rel="stylesheet" href="{% static "css/base/notification.css" %}">
{% block head %}{% endblock %}
</head>
@@ -38,18 +37,6 @@
<!-- Git hash for this release: {{ git_sha }} -->
<main class="site-content">
- {% if messages %}
- <div class="messages">
- {% for message in messages %}
- <div class="notification {% if message.tags %}is-{{ message.tags }}{% endif %}">
- <button class="delete"></button>
-
- {{ message }}
- </div>
- {% endfor %}
- </div>
- {% endif %}
-
{% block content %}
{{ block.super }}
{% endblock %}
diff --git a/pydis_site/templates/base/navbar.html b/pydis_site/templates/base/navbar.html
index dd68949b..6c8d52a1 100644
--- a/pydis_site/templates/base/navbar.html
+++ b/pydis_site/templates/base/navbar.html
@@ -1,4 +1,3 @@
-{% load socialaccount %}
{% load static %}
<nav class="navbar is-primary" role="navigation" aria-label="main navigation">
@@ -93,33 +92,6 @@
<a class="navbar-item" href="#">
All events
</a>
- <hr class="navbar-divider">
-
- {% if not user.is_authenticated %}
- {% get_providers as socialaccount_providers %}
-
- {% for provider in socialaccount_providers %}
- {% if provider.id == "discord" %}
- <a class="navbar-item"
- href="{% provider_login_url provider.id process="login" scope=scope auth_params=auth_params %}"
- >Login with {{ provider.name }}</a>
- {% endif %}
- {% endfor %}
- {% else %}
- <form method="post" action="{% url 'logout' %}">
- {% csrf_token %}
-
- <div class="field navbar-item is-paddingless is-fullwidth is-grouped">
- <button type="submit" class="button is-white is-inline is-fullwidth has-text-left is-size-navbar-menu has-text-grey-dark">Logout</button>
- <a title="Settings" class="button is-white is-inline has-text-right is-size-navbar-menu has-text-grey-dark modal-button" data-target="account-modal">
- <span class="is-icon">
- <i class="fas fa-cog"></i>
- </span>
- </a>
- </div>
- </form>
- {% endif %}
-
</div>
</div>
</div>
@@ -130,24 +102,3 @@
</a>
</div>
</nav>
-
-{% if user.is_authenticated %}
- <script defer type="text/javascript">
- // Script which loads and sets up the account settings modal.
- // This script must be placed in a template, or rewritten to take the fetch
- // URL as a function argument, in order to be used.
-
- "use strict";
-
- // Create and prepend a new div for this modal
- let element = document.createElement("div");
- document.body.prepend(element);
-
- fetch("{% url "account_settings" %}") // Fetch the URL
- .then((response) => response.text()) // Read in the data stream as text
- .then((text) => {
- element.outerHTML = text; // Replace the div's HTML with the loaded modal HTML
- setupModal(document.getElementById("account-modal")); // Set up the modal
- });
- </script>
-{% endif %}
diff --git a/pydis_site/templates/home/account/delete.html b/pydis_site/templates/home/account/delete.html
deleted file mode 100644
index 0d44e32a..00000000
--- a/pydis_site/templates/home/account/delete.html
+++ /dev/null
@@ -1,47 +0,0 @@
-{% extends 'base/base.html' %}
-{% load static %}
-
-{% block title %}Delete Account{% endblock %}
-
-{% block content %}
- {% include "base/navbar.html" %}
-
- <section class="section content">
- <div class="container">
- <h2 class="is-size-2 has-text-centered">Account Deletion</h2>
-
- <div class="columns is-centered">
- <div class="column is-half-desktop is-full-tablet is-full-mobile">
-
- <article class="message is-danger">
- <div class="message-body">
- <p>
- You have requested to delete the account with username
- <strong><span class="has-text-dark is-family-monospace">{{ user.username }}</span></strong>.
- </p>
-
- <p>
- Please note that this <strong>cannot be undone</strong>.
- </p>
-
- <p>
- To verify that you'd like to remove your account, please type your username into the box below.
- </p>
- </div>
- </article>
- </div>
- </div>
-
- <div class="columns is-centered">
- <div class="column is-half-desktop is-full-tablet is-full-mobile">
- <form method="post">
- {% csrf_token %}
- <label for="id_username" class="label requiredField">Username</label>
- <input id="id_username" class="input" type="text" required name="username">
- <input style="margin-top: 1em;" type="submit" value="I understand, delete my account" class="button is-primary">
- </form>
- </div>
- </div>
- </div>
- </section>
-{% endblock %}
diff --git a/pydis_site/templates/home/account/settings.html b/pydis_site/templates/home/account/settings.html
deleted file mode 100644
index ed59b052..00000000
--- a/pydis_site/templates/home/account/settings.html
+++ /dev/null
@@ -1,136 +0,0 @@
-{% load socialaccount %}
-
-{# This template is just for a modal, which is actually inserted into the navbar #}
-{# template. Take a look at `navbar.html` to see how it's inserted. #}
-
-<div class="modal" id="account-modal">
- <div class="modal-background"></div>
- <div class="modal-card">
- <div class="modal-card-head">
- <div class="modal-card-title">Settings for {{ user.username }}</div>
-
- {% if groups %}
- <div>
- {% for group in groups %}
- <span class="tag is-primary">{{ group.name }}</span>
- {% endfor %}
- </div>
- {% else %}
- <span class="tag is-dark">No groups</span>
- {% endif %}
- </div>
- <div class="modal-card-body">
- <h3 class="title">Connections</h3>
- <div class="columns">
- {% if discord_provider is not None %}
- <div class="column">
- <div class="box">
- {% if not discord %}
- <div class="media">
- <div class="media-left">
- <div class="image">
- <i class="fab fa-discord fa-3x has-text-primary"></i>
- </div>
- </div>
- <div class="media-content">
- <div class="title is-5">Discord</div>
- <div class="subtitle is-6">Not connected</div>
- </div>
- </div>
- <div>
- <br />
- <a class="button is-primary" href="{% provider_login_url "discord" process="connect" %}">
- <span class="icon">
- <i class="fad fa-link"></i>
- </span>
- <span>Connect</span>
- </a>
- </div>
- {% else %}
- <div class="media">
- <div class="media-left">
- <div class="image">
- <i class="fab fa-discord fa-3x has-text-primary"></i>
- </div>
- </div>
- <div class="media-content">
- <div class="title is-5">Discord</div>
- <div class="subtitle is-6">{{ user.username }}</div>
- </div>
- </div>
- <div>
- <br />
- <button class="button" disabled>
- <span class="icon">
- <i class="fas fa-check"></i>
- </span>
- <span>Connected</span>
- </button>
- </div>
- {% endif %}
- </div>
- </div>
- {% endif %}
-
- {% if github_provider is not None %}
- <div class="column">
- <div class="box">
- {% if not github %}
- <div class="media">
- <div class="media-left">
- <div class="image">
- <i class="fab fa-github fa-3x"></i>
- </div>
- </div>
- <div class="media-content">
- <div class="title is-5">GitHub</div>
- <div class="subtitle is-6">Not connected</div>
- </div>
- </div>
- <div>
- <br />
- <a class="button is-primary" href="{% provider_login_url "github" process="connect" %}">
- <span class="icon">
- <i class="fad fa-link"></i>
- </span>
- <span>Connect</span>
- </a>
- </div>
- {% else %}
- <div class="media">
- <div class="media-left">
- <div class="image">
- <i class="fab fa-github fa-3x"></i>
- </div>
- </div>
- <div class="media-content">
- <div class="title is-5">GitHub</div>
- <div class="subtitle is-6">{{ github.extra_data.name }}</div>
- </div>
- </div>
- <div>
- <form method="post" action="{% url "account_settings" %}" type="submit">
- {% csrf_token %}
-
- <input type="hidden" name="provider" value="github" />
-
- <br />
- <button type="submit" class="button is-danger">
- <span class="icon">
- <i class="fas fa-times"></i>
- </span>
- <span>Disconnect</span>
- </button>
- </form>
- </div>
- {% endif %}
- </div>
- </div>
- {% endif %}
- </div>
- </div>
- <div class="modal-card-foot">
- <a class="button is-danger" href="{% url "account_delete" %}">Delete Account</a>
- </div>
- </div>
-</div>
diff --git a/pydis_site/templates/resources/resources.html b/pydis_site/templates/resources/resources.html
new file mode 100644
index 00000000..6eb32c97
--- /dev/null
+++ b/pydis_site/templates/resources/resources.html
@@ -0,0 +1,90 @@
+{% extends 'base/base.html' %}
+{% load static %}
+
+{% block title %}Resources{% endblock %}
+{% block head %}
+ <link rel="stylesheet" href="{% static "css/resources/resources.css" %}">
+{% endblock %}
+
+{% block content %}
+ {% include "base/navbar.html" %}
+
+ <section class="section">
+ <div class="container">
+ <div class="content">
+ <h1>Resources</h1>
+
+ <div class="tile is-ancestor">
+ <a class="tile is-parent" href="/articles/category/guides">
+ <article class="tile is-child box hero is-primary is-bold">
+ <p class="title is-size-1"><i class="fad fa-info-circle" aria-hidden="true"></i> Guides</p>
+ <p class="subtitle is-size-4">Made by us, for you</p>
+ </article>
+ </a>
+
+ <div class="tile is-vertical is-9">
+ <div class="tile">
+ <a class="tile is-8 is-parent" href="/resources/reading/">
+ <article class="tile is-child box hero is-black" id="readingBlock">
+ <p class="title is-size-1"><i class="fad fa-book-alt" aria-hidden="true"></i> Read</p>
+ <p class="subtitle is-size-4">Lovingly curated books to explore</p>
+ </article>
+ </a>
+
+ <div class="tile">
+ <a class="tile is-parent" href="/resources/videos/">
+ <article class="tile is-child box hero is-danger is-bold">
+ <p class="title is-size-1"><i class="fad fa-video" aria-hidden="true"></i> Watch</p>
+ <p class="subtitle is-size-4">Visually engaging</p>
+ </article>
+ </a>
+ </div>
+ </div>
+
+ <div class="tile">
+ <a class="tile is-parent" href="/resources/interactive/">
+ <article class="tile is-child box hero is-black" id="interactiveBlock">
+ <p class="title is-size-1"><i class="fad fa-code" aria-hidden="true"></i> Try</p>
+ <p class="subtitle is-size-4">Interactively discover the possibilities</p>
+ </article>
+ </a>
+ <a class="tile is-8 is-parent" href="/resources/courses/">
+ <article class="tile is-child box hero is-success is-bold">
+ <p class="title is-size-1"><i class="fad fa-graduation-cap" aria-hidden="true"></i> Learn</p>
+ <p class="subtitle is-size-4">Structured courses with clear goals</p>
+ </article>
+ </a>
+ </div>
+ </div>
+ </div>
+
+ <div class="tile is-ancestor">
+ <div class="tile is-vertical is-9">
+ <div class="tile">
+ <a class="tile is-8 is-parent" href="/resources/communities/">
+ <article class="tile is-child box hero is-black" id="communitiesBlock">
+ <p class="title is-size-1"><i class="fad fa-users" aria-hidden="true"></i> Communities</p>
+ <p class="subtitle is-size-4">Some of our best friends</p>
+ </article>
+ </a>
+ <div class="tile">
+ <a class="tile is-parent" href="/resources/podcasts/">
+ <article class="tile is-child box hero is-black" id="podcastsBlock">
+ <p class="title is-size-1"><i class="fad fa-podcast" aria-hidden="true"></i> Listen</p>
+ <p class="subtitle is-size-4">Regular podcasts to follow</p>
+ </article>
+ </a>
+ </div>
+ </div>
+ </div>
+ <a class="tile is-parent" href="/resources/tools/">
+ <article class="tile is-child box hero is-dark">
+ <p class="title is-size-1"><i class="fad fa-tools" aria-hidden="true"></i> Tools</p>
+ <p class="subtitle is-size-4">Things we love to use</p>
+ </article>
+ </a>
+ </div>
+ </div>
+ </div>
+ </section>
+{% endblock %}
diff --git a/pydis_site/tests/test_utils_account.py b/pydis_site/tests/test_utils_account.py
deleted file mode 100644
index 6f8338b4..00000000
--- a/pydis_site/tests/test_utils_account.py
+++ /dev/null
@@ -1,139 +0,0 @@
-from unittest.mock import patch
-
-from allauth.exceptions import ImmediateHttpResponse
-from allauth.socialaccount.models import SocialAccount, SocialLogin
-from django.contrib.auth.models import User
-from django.contrib.messages.storage.base import BaseStorage
-from django.http import HttpRequest
-from django.test import RequestFactory, TestCase
-
-from pydis_site.apps.api.models import Role, User as DiscordUser
-from pydis_site.utils.account import AccountAdapter, SocialAccountAdapter
-
-
-class AccountUtilsTests(TestCase):
- def setUp(self):
- # Create the user
- self.django_user = User.objects.create(username="user")
-
- # Create the roles
- developers_role = Role.objects.create(
- id=1,
- name="Developers",
- colour=0,
- permissions=0,
- position=1
- )
- everyone_role = Role.objects.create(
- id=0,
- name="@everyone",
- colour=0,
- permissions=0,
- position=0
- )
-
- # Create the social accounts
- self.discord_account = SocialAccount.objects.create(
- user=self.django_user, provider="discord", uid=0
- )
- self.discord_account_one_role = SocialAccount.objects.create(
- user=self.django_user, provider="discord", uid=1
- )
- self.discord_account_two_roles = SocialAccount.objects.create(
- user=self.django_user, provider="discord", uid=2
- )
- self.discord_account_not_present = SocialAccount.objects.create(
- user=self.django_user, provider="discord", uid=3
- )
- self.github_account = SocialAccount.objects.create(
- user=self.django_user, provider="github", uid=0
- )
-
- # Create DiscordUsers
- self.discord_user = DiscordUser.objects.create(
- id=0,
- name="user",
- discriminator=0
- )
-
- self.discord_user_role = DiscordUser.objects.create(
- id=1,
- name="user present",
- discriminator=0,
- roles=[everyone_role.id]
- )
-
- self.discord_user_two_roles = DiscordUser.objects.create(
- id=2,
- name="user with both roles",
- discriminator=0,
- roles=[everyone_role.id, developers_role.id]
- )
-
- self.request_factory = RequestFactory()
-
- def test_account_adapter(self):
- """Test that our Allauth account adapter functions correctly."""
- adapter = AccountAdapter()
-
- self.assertFalse(adapter.is_open_for_signup(HttpRequest()))
-
- def test_social_account_adapter_signup(self):
- """Test that our Allauth social account adapter correctly handles signups."""
- adapter = SocialAccountAdapter()
-
- discord_login = SocialLogin(account=self.discord_account)
- discord_login_role = SocialLogin(account=self.discord_account_one_role)
- discord_login_not_present = SocialLogin(account=self.discord_account_not_present)
- discord_login_two_roles = SocialLogin(account=self.discord_account_two_roles)
-
- github_login = SocialLogin(account=self.github_account)
-
- messages_request = self.request_factory.get("/")
- messages_request._messages = BaseStorage(messages_request)
-
- with patch("pydis_site.utils.account.reverse") as mock_reverse:
- with patch("pydis_site.utils.account.redirect") as mock_redirect:
- with self.assertRaises(ImmediateHttpResponse):
- adapter.is_open_for_signup(messages_request, github_login)
-
- with self.assertRaises(ImmediateHttpResponse):
- adapter.is_open_for_signup(messages_request, discord_login)
-
- with self.assertRaises(ImmediateHttpResponse):
- adapter.is_open_for_signup(messages_request, discord_login_role)
-
- with self.assertRaises(ImmediateHttpResponse):
- adapter.is_open_for_signup(messages_request, discord_login_not_present)
-
- self.assertTrue(
- adapter.is_open_for_signup(messages_request, discord_login_two_roles)
- )
-
- self.assertEqual(len(messages_request._messages._queued_messages), 4)
- self.assertEqual(mock_redirect.call_count, 4)
- self.assertEqual(mock_reverse.call_count, 4)
-
- def test_social_account_adapter_populate(self):
- """Test that our Allauth social account adapter correctly handles data population."""
- adapter = SocialAccountAdapter()
-
- discord_login = SocialLogin(
- account=self.discord_account,
- user=self.django_user
- )
- discord_login.account.extra_data["discriminator"] = "0000"
-
- discord_user = adapter.populate_user(
- self.request_factory.get("/"), discord_login,
- {"username": "user"}
- )
- self.assertEqual(discord_user.username, "user#0000")
- self.assertEqual(discord_user.first_name, "user#0000")
-
- discord_login.account.provider = "not_discord"
- not_discord_user = adapter.populate_user(
- self.request_factory.get("/"), discord_login,
- {"username": "user"}
- )
- self.assertEqual(not_discord_user.username, "user")
diff --git a/pydis_site/utils/account.py b/pydis_site/utils/account.py
deleted file mode 100644
index b4e41198..00000000
--- a/pydis_site/utils/account.py
+++ /dev/null
@@ -1,79 +0,0 @@
-from typing import Any, Dict
-
-from allauth.account.adapter import DefaultAccountAdapter
-from allauth.exceptions import ImmediateHttpResponse
-from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
-from allauth.socialaccount.models import SocialLogin
-from django.contrib.auth.models import User as DjangoUser
-from django.contrib.messages import ERROR, add_message
-from django.http import HttpRequest
-from django.shortcuts import redirect
-from django.urls import reverse
-
-from pydis_site.apps.api.models import User as DiscordUser
-
-ERROR_CONNECT_DISCORD = ("You must login with Discord before connecting another account. "
- "Your account details have not been saved.")
-ERROR_JOIN_DISCORD = ("Please join the Discord server and verify that you accept the rules and "
- "privacy policy.")
-
-
-class AccountAdapter(DefaultAccountAdapter):
- """An Allauth account adapter that prevents signups via form submission."""
-
- def is_open_for_signup(self, request: HttpRequest) -> bool:
- """
- Checks whether or not the site is open for signups.
-
- We override this to always return False so that users may never sign up using
- Allauth's signup form endpoints, to be on the safe side - since we only want users
- to sign up using their Discord account.
- """
- return False
-
-
-class SocialAccountAdapter(DefaultSocialAccountAdapter):
- """An Allauth SocialAccount adapter that prevents signups via non-Discord connections."""
-
- def is_open_for_signup(self, request: HttpRequest, social_login: SocialLogin) -> bool:
- """
- Checks whether or not the site is open for signups.
-
- We override this method in order to prevent users from creating a new account using
- a non-Discord connection, as we require this connection for our users.
- """
- if social_login.account.provider != "discord":
- add_message(request, ERROR, ERROR_CONNECT_DISCORD)
-
- raise ImmediateHttpResponse(redirect(reverse("home")))
-
- try:
- user = DiscordUser.objects.get(id=int(social_login.account.uid))
- except DiscordUser.DoesNotExist:
- add_message(request, ERROR, ERROR_JOIN_DISCORD)
-
- raise ImmediateHttpResponse(redirect(reverse("home")))
-
- if len(user.roles) <= 1:
- add_message(request, ERROR, ERROR_JOIN_DISCORD)
-
- raise ImmediateHttpResponse(redirect(reverse("home")))
-
- return True
-
- def populate_user(self, request: HttpRequest,
- social_login: SocialLogin,
- data: Dict[str, Any]) -> DjangoUser:
- """
- Method used to populate a Django User with data.
-
- We override this so that the Django user is created with the username#discriminator,
- instead of just the username, as Django users must have unique usernames. For display
- purposes, we also set the `name` key, which is used for `first_name` in the database.
- """
- if social_login.account.provider == "discord":
- discriminator = social_login.account.extra_data["discriminator"]
- data["username"] = f"{data['username']}#{discriminator:0>4}"
- data["name"] = data["username"]
-
- return super().populate_user(request, social_login, data)
diff --git a/pydis_site/utils/views.py b/pydis_site/utils/views.py
deleted file mode 100644
index c9803bd6..00000000
--- a/pydis_site/utils/views.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from django.contrib import messages
-from django.http import HttpRequest
-from django.views.generic import RedirectView
-
-
-class MessageRedirectView(RedirectView):
- """
- Redirects to another URL, also setting a message using the Django Messages framework.
-
- This is based on Django's own `RedirectView` and works the same way, but takes two additional
- parameters.
-
- * `message`: Set to the message content you wish to display.
- * `message_level`: Set to one of the message levels from the Django messages framework. This
- parameter defaults to `messages.INFO`.
- """
-
- message: str = ""
- message_level: int = messages.INFO
-
- def get(self, request: HttpRequest, *args, **kwargs) -> None:
- """Called upon a GET request."""
- messages.add_message(request, self.message_level, self.message)
-
- return super().get(request, *args, **kwargs)