aboutsummaryrefslogtreecommitdiffstats
path: root/pydis_site/apps
diff options
context:
space:
mode:
authorGravatar Joe Banks <[email protected]>2021-05-14 17:46:00 +0100
committerGravatar GitHub <[email protected]>2021-05-14 17:46:00 +0100
commit2836913d98e762d819d6d50ec668112077e8a3a9 (patch)
treeccb02577b40ad4c92b56c8c6d3ae1c53e90afe26 /pydis_site/apps
parentMerge pull request #486 from python-discord/bast0006-patch-error-page-margins (diff)
parentRemove stale dependency. (diff)
Merge pull request #487 from python-discord/dewikification
Diffstat (limited to 'pydis_site/apps')
-rw-r--r--pydis_site/apps/content/__init__.py (renamed from pydis_site/apps/home/forms/__init__.py)0
-rw-r--r--pydis_site/apps/content/apps.py7
-rw-r--r--pydis_site/apps/content/migrations/__init__.py0
-rw-r--r--pydis_site/apps/content/resources/_info.yml2
-rw-r--r--pydis_site/apps/content/resources/code-of-conduct.md100
-rw-r--r--pydis_site/apps/content/resources/frequently-asked-questions.md131
-rw-r--r--pydis_site/apps/content/resources/guides/_info.yml2
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/_info.yml2
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/asking-good-questions.md180
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/code-reviews-primer.md165
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing.md115
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/_info.yml3
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/bot.md209
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/cloning-repository.md31
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/configure-environment-variables.md23
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines.md30
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines/_info.yml2
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines/supplemental-information.md97
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/docker.md120
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/forking-repository.md18
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/hosts-file.md46
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/installing-project-dependencies.md38
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/issues.md121
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/setting-test-server-and-bot-account.md68
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot.md119
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot/_info.yml2
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot/env-var-reference.md68
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/site.md144
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/style-guide.md211
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git.md23
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/_info.yml3
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/cli.md121
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/pycharm.md67
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/help-channel-guide.md78
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/helping-others.md140
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/how-to-contribute-a-page.md217
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/off-topic-etiquette.md32
-rw-r--r--pydis_site/apps/content/resources/guides/python-guides/_info.yml3
-rw-r--r--pydis_site/apps/content/resources/guides/python-guides/creating-python-environment-windows.md72
-rw-r--r--pydis_site/apps/content/resources/guides/python-guides/discordpy.md230
-rw-r--r--pydis_site/apps/content/resources/guides/python-guides/mutability.md55
-rw-r--r--pydis_site/apps/content/resources/guides/python-guides/parameters-and-arguments.md290
-rw-r--r--pydis_site/apps/content/resources/privacy.md12
-rw-r--r--pydis_site/apps/content/resources/rules.md44
-rw-r--r--pydis_site/apps/content/resources/security-notice.md37
-rw-r--r--pydis_site/apps/content/resources/server-info/_info.yml3
-rw-r--r--pydis_site/apps/content/resources/server-info/roles.md131
-rw-r--r--pydis_site/apps/content/resources/server-info/staff-role-expectations.md67
-rw-r--r--pydis_site/apps/content/tests/__init__.py0
-rw-r--r--pydis_site/apps/content/tests/helpers.py91
-rw-r--r--pydis_site/apps/content/tests/test_utils.py93
-rw-r--r--pydis_site/apps/content/tests/test_views.py184
-rw-r--r--pydis_site/apps/content/urls.py9
-rw-r--r--pydis_site/apps/content/utils.py64
-rw-r--r--pydis_site/apps/content/views/__init__.py3
-rw-r--r--pydis_site/apps/content/views/page_category.py95
-rw-r--r--pydis_site/apps/events/__init__.py0
-rw-r--r--pydis_site/apps/events/apps.py7
-rw-r--r--pydis_site/apps/events/migrations/__init__.py0
-rw-r--r--pydis_site/apps/events/tests/__init__.py0
-rw-r--r--pydis_site/apps/events/tests/test_views.py42
-rw-r--r--pydis_site/apps/events/urls.py9
-rw-r--r--pydis_site/apps/events/views/__init__.py4
-rw-r--r--pydis_site/apps/events/views/index.py7
-rw-r--r--pydis_site/apps/events/views/page.py25
-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/resources/books/byte_of_python.yaml17
-rw-r--r--pydis_site/apps/home/resources/communities/adafruit.yaml14
-rw-r--r--pydis_site/apps/home/resources/communities/functional_programming.yaml10
-rw-r--r--pydis_site/apps/home/resources/communities/pallets.yaml10
-rw-r--r--pydis_site/apps/home/resources/communities/rlbot.yaml11
-rw-r--r--pydis_site/apps/home/resources/communities/subreddit.yaml8
-rw-r--r--pydis_site/apps/home/resources/editors/atom.yaml12
-rw-r--r--pydis_site/apps/home/resources/editors/mu_editor.yaml12
-rw-r--r--pydis_site/apps/home/resources/editors/sublime_text.yaml9
-rw-r--r--pydis_site/apps/home/resources/editors/visual_studio_code.yaml11
-rw-r--r--pydis_site/apps/home/resources/ides/_category_info.yaml2
-rw-r--r--pydis_site/apps/home/resources/ides/pycharm.yaml10
-rw-r--r--pydis_site/apps/home/resources/ides/spyder.yaml12
-rw-r--r--pydis_site/apps/home/resources/ides/thonny.yaml12
-rw-r--r--pydis_site/apps/home/resources/interactive_learning_tools/_category_info.yaml3
-rw-r--r--pydis_site/apps/home/resources/interactive_learning_tools/automate_the_boring_stuff.yaml11
-rw-r--r--pydis_site/apps/home/resources/interactive_learning_tools/learn_to_program.yaml13
-rw-r--r--pydis_site/apps/home/resources/interactive_learning_tools/mit_python.yaml10
-rw-r--r--pydis_site/apps/home/resources/interactive_learning_tools/programming_for_everybody.yaml10
-rw-r--r--pydis_site/apps/home/resources/misc/_category_info.yaml2
-rw-r--r--pydis_site/apps/home/resources/misc/good_first_issue_tag.yaml10
-rw-r--r--pydis_site/apps/home/resources/misc/python_cheat_sheet.yaml9
-rw-r--r--pydis_site/apps/home/resources/podcasts/_category_info.yaml2
-rw-r--r--pydis_site/apps/home/resources/tutorials/_category_info.yaml3
-rw-r--r--pydis_site/apps/home/resources/tutorials/corey_schafer.yaml7
-rw-r--r--pydis_site/apps/home/resources/tutorials/get_started_with_flask.yaml9
-rw-r--r--pydis_site/apps/home/resources/tutorials/getting_started_with_python.yaml12
-rw-r--r--pydis_site/apps/home/resources/tutorials/hitchhikers_guide_to_python.yaml10
-rw-r--r--pydis_site/apps/home/resources/tutorials/sentdex.yaml8
-rw-r--r--pydis_site/apps/home/resources/tutorials/simple_guide_to_git.yaml8
-rw-r--r--pydis_site/apps/home/signals.py314
-rw-r--r--pydis_site/apps/home/templatetags/extra_filters.py2
-rw-r--r--pydis_site/apps/home/templatetags/wiki_extra.py132
-rw-r--r--pydis_site/apps/home/tests/test_signal_listener.py458
-rw-r--r--pydis_site/apps/home/tests/test_views.py221
-rw-r--r--pydis_site/apps/home/tests/test_wiki_templatetags.py238
-rw-r--r--pydis_site/apps/home/urls.py39
-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/redirect/__init__.py0
-rw-r--r--pydis_site/apps/redirect/apps.py7
-rw-r--r--pydis_site/apps/redirect/migrations/__init__.py0
-rw-r--r--pydis_site/apps/redirect/redirects.yaml194
-rw-r--r--pydis_site/apps/redirect/tests.py61
-rw-r--r--pydis_site/apps/redirect/urls.py19
-rw-r--r--pydis_site/apps/redirect/views.py26
-rw-r--r--pydis_site/apps/resources/__init__.py0
-rw-r--r--pydis_site/apps/resources/apps.py7
-rw-r--r--pydis_site/apps/resources/migrations/__init__.py0
-rw-r--r--pydis_site/apps/resources/resources/communities/_category_info.yaml (renamed from pydis_site/apps/home/resources/communities/_category_info.yaml)2
-rw-r--r--pydis_site/apps/resources/resources/communities/adafruit.yaml15
-rw-r--r--pydis_site/apps/resources/resources/communities/awesome_programming_discord.yaml9
-rw-r--r--pydis_site/apps/resources/resources/communities/kivy.yaml18
-rw-r--r--pydis_site/apps/resources/resources/communities/microsoft.yaml12
-rw-r--r--pydis_site/apps/resources/resources/communities/pallets.yaml13
-rw-r--r--pydis_site/apps/resources/resources/communities/panda3d.yaml12
-rw-r--r--pydis_site/apps/resources/resources/communities/people_postgres_data.yaml18
-rw-r--r--pydis_site/apps/resources/resources/communities/pyglet.yaml15
-rw-r--r--pydis_site/apps/resources/resources/communities/real_python.yaml12
-rw-r--r--pydis_site/apps/resources/resources/communities/rlbot.yaml13
-rw-r--r--pydis_site/apps/resources/resources/communities/subreddit.yaml6
-rw-r--r--pydis_site/apps/resources/resources/courses/_category_info.yaml4
-rw-r--r--pydis_site/apps/resources/resources/courses/automate_the_boring_stuff_with_python.yaml5
-rw-r--r--pydis_site/apps/resources/resources/courses/mit_introduction_to_computer_science_and_programming.yaml6
-rw-r--r--pydis_site/apps/resources/resources/courses/practical_python_programming.yaml9
-rw-r--r--pydis_site/apps/resources/resources/courses/university_of_michigan.yaml5
-rw-r--r--pydis_site/apps/resources/resources/courses/university_of_toronto.yaml11
-rw-r--r--pydis_site/apps/resources/resources/interactive/_category_info.yaml4
-rw-r--r--pydis_site/apps/resources/resources/interactive/code_combat.yaml (renamed from pydis_site/apps/home/resources/interactive_learning_tools/code_combat.yaml)8
-rw-r--r--pydis_site/apps/resources/resources/interactive/edublocks.yaml10
-rw-r--r--pydis_site/apps/resources/resources/interactive/exercism.yaml (renamed from pydis_site/apps/home/resources/interactive_learning_tools/exercism.yaml)7
-rw-r--r--pydis_site/apps/resources/resources/interactive/jetbrains_academy.yaml8
-rw-r--r--pydis_site/apps/resources/resources/interactive/python_morsels.yaml (renamed from pydis_site/apps/home/resources/interactive_learning_tools/python_morsels.yaml)9
-rw-r--r--pydis_site/apps/resources/resources/interactive/python_tutor.yaml4
-rw-r--r--pydis_site/apps/resources/resources/interactive/sololearn.yaml7
-rw-r--r--pydis_site/apps/resources/resources/podcasts/_category_info.yaml4
-rw-r--r--pydis_site/apps/resources/resources/podcasts/podcast_dunder_init.yaml (renamed from pydis_site/apps/home/resources/podcasts/podcast_dunder_init.yaml)8
-rw-r--r--pydis_site/apps/resources/resources/podcasts/python_bytes.yaml (renamed from pydis_site/apps/home/resources/podcasts/python_bytes.yaml)8
-rw-r--r--pydis_site/apps/resources/resources/podcasts/talk_python_to_me.yaml (renamed from pydis_site/apps/home/resources/podcasts/talk_python_to_me.yaml)8
-rw-r--r--pydis_site/apps/resources/resources/podcasts/test_and_code.yaml5
-rw-r--r--pydis_site/apps/resources/resources/podcasts/the_real_python_podcast.yaml7
-rw-r--r--pydis_site/apps/resources/resources/reading/_category_info.yaml2
-rw-r--r--pydis_site/apps/resources/resources/reading/books/_category_info.yaml (renamed from pydis_site/apps/home/resources/books/_category_info.yaml)5
-rw-r--r--pydis_site/apps/resources/resources/reading/books/automate_the_boring_stuff.yaml (renamed from pydis_site/apps/home/resources/books/automate_the_boring_stuff.yaml)10
-rw-r--r--pydis_site/apps/resources/resources/reading/books/byte_of_python.yaml15
-rw-r--r--pydis_site/apps/resources/resources/reading/books/effective_python.yaml (renamed from pydis_site/apps/home/resources/books/effective_python.yaml)9
-rw-r--r--pydis_site/apps/resources/resources/reading/books/flask_web_development.yaml (renamed from pydis_site/apps/home/resources/books/flask_web_development.yaml)8
-rw-r--r--pydis_site/apps/resources/resources/reading/books/fluent_python.yaml (renamed from pydis_site/apps/home/resources/books/fluent_python.yaml)10
-rw-r--r--pydis_site/apps/resources/resources/reading/books/hitchhikers_guide_to_python.yaml11
-rw-r--r--pydis_site/apps/resources/resources/reading/books/inferential_thinking.yaml9
-rw-r--r--pydis_site/apps/resources/resources/reading/books/mission_python.yaml (renamed from pydis_site/apps/home/resources/books/mission_python.yaml)6
-rw-r--r--pydis_site/apps/resources/resources/reading/books/neural_networks_from_scratch_in_python.yaml10
-rw-r--r--pydis_site/apps/resources/resources/reading/books/python_cookbook.yaml (renamed from pydis_site/apps/home/resources/books/python_cookbook.yaml)12
-rw-r--r--pydis_site/apps/resources/resources/reading/books/python_crash_course.yaml20
-rw-r--r--pydis_site/apps/resources/resources/reading/books/python_tricks.yaml (renamed from pydis_site/apps/home/resources/books/python_tricks.yaml)6
-rw-r--r--pydis_site/apps/resources/resources/reading/books/think_python.yaml17
-rw-r--r--pydis_site/apps/resources/resources/reading/books/two_scoops_of_django.yaml (renamed from pydis_site/apps/home/resources/books/two_scoops_of_django.yaml)12
-rw-r--r--pydis_site/apps/resources/resources/reading/tutorials/_category_info.yaml5
-rw-r--r--pydis_site/apps/resources/resources/reading/tutorials/getting_started_with_kivy.yaml5
-rw-r--r--pydis_site/apps/resources/resources/reading/tutorials/getting_started_with_python_for_non_programmers.yaml5
-rw-r--r--pydis_site/apps/resources/resources/reading/tutorials/getting_started_with_python_for_programmers.yaml5
-rw-r--r--pydis_site/apps/resources/resources/reading/tutorials/python_cheat_sheet.yaml5
-rw-r--r--pydis_site/apps/resources/resources/reading/tutorials/python_developer_guide.yaml5
-rw-r--r--pydis_site/apps/resources/resources/reading/tutorials/simple_guide_to_git.yaml6
-rw-r--r--pydis_site/apps/resources/resources/reading/tutorials/the_flask_mega_tutorial.yaml4
-rw-r--r--pydis_site/apps/resources/resources/reading/tutorials/wtf_python.yaml8
-rw-r--r--pydis_site/apps/resources/resources/tools/_category_info.yaml4
-rw-r--r--pydis_site/apps/resources/resources/tools/accessibility_tools/_category_info.yaml5
-rw-r--r--pydis_site/apps/resources/resources/tools/accessibility_tools/screen_readers.yaml7
-rw-r--r--pydis_site/apps/resources/resources/tools/accessibility_tools/talon_voice.yaml6
-rw-r--r--pydis_site/apps/resources/resources/tools/editors/_category_info.yaml (renamed from pydis_site/apps/home/resources/editors/_category_info.yaml)3
-rw-r--r--pydis_site/apps/resources/resources/tools/editors/atom.yaml5
-rw-r--r--pydis_site/apps/resources/resources/tools/editors/google_collab.yaml7
-rw-r--r--pydis_site/apps/resources/resources/tools/editors/mu_editor.yaml7
-rw-r--r--pydis_site/apps/resources/resources/tools/editors/sublime_text.yaml5
-rw-r--r--pydis_site/apps/resources/resources/tools/editors/visual_studio_code.yaml4
-rw-r--r--pydis_site/apps/resources/resources/tools/ides/_category_info.yaml5
-rw-r--r--pydis_site/apps/resources/resources/tools/ides/pycharm.yaml5
-rw-r--r--pydis_site/apps/resources/resources/tools/ides/repl_it.yaml5
-rw-r--r--pydis_site/apps/resources/resources/tools/ides/spyder.yaml5
-rw-r--r--pydis_site/apps/resources/resources/tools/ides/thonny.yaml5
-rw-r--r--pydis_site/apps/resources/resources/videos/_category_info.yaml2
-rw-r--r--pydis_site/apps/resources/resources/videos/corey_schafer.yaml19
-rw-r--r--pydis_site/apps/resources/resources/videos/jetbrains.yaml12
-rw-r--r--pydis_site/apps/resources/resources/videos/jim_shaped_coding.yaml13
-rw-r--r--pydis_site/apps/resources/resources/videos/microsoft.yaml18
-rw-r--r--pydis_site/apps/resources/resources/videos/python_discord.yaml8
-rw-r--r--pydis_site/apps/resources/resources/videos/sentdex.yaml22
-rw-r--r--pydis_site/apps/resources/templatetags/__init__.py3
-rw-r--r--pydis_site/apps/resources/templatetags/as_icon.py14
-rw-r--r--pydis_site/apps/resources/tests/__init__.py0
-rw-r--r--pydis_site/apps/resources/tests/test_as_icon.py28
-rw-r--r--pydis_site/apps/resources/tests/test_views.py34
-rw-r--r--pydis_site/apps/resources/tests/testing_resources/testing/_category_info.yaml1
-rw-r--r--pydis_site/apps/resources/tests/testing_resources/testing/foobar/_category_info.yaml1
-rw-r--r--pydis_site/apps/resources/tests/testing_resources/testing/foobar/resource_test.yaml1
-rw-r--r--pydis_site/apps/resources/tests/testing_resources/testing/my_resource.yaml1
-rw-r--r--pydis_site/apps/resources/urls.py9
-rw-r--r--pydis_site/apps/resources/utils.py42
-rw-r--r--pydis_site/apps/resources/views/__init__.py4
-rw-r--r--pydis_site/apps/resources/views/resources.py7
-rw-r--r--pydis_site/apps/resources/views/resources_list.py39
-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/apps/staff/urls.py4
217 files changed, 5427 insertions, 1940 deletions
diff --git a/pydis_site/apps/home/forms/__init__.py b/pydis_site/apps/content/__init__.py
index e69de29b..e69de29b 100644
--- a/pydis_site/apps/home/forms/__init__.py
+++ b/pydis_site/apps/content/__init__.py
diff --git a/pydis_site/apps/content/apps.py b/pydis_site/apps/content/apps.py
new file mode 100644
index 00000000..1e300a48
--- /dev/null
+++ b/pydis_site/apps/content/apps.py
@@ -0,0 +1,7 @@
+from django.apps import AppConfig
+
+
+class ContentConfig(AppConfig):
+ """Django AppConfig for content app."""
+
+ name = 'content'
diff --git a/pydis_site/apps/content/migrations/__init__.py b/pydis_site/apps/content/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/pydis_site/apps/content/migrations/__init__.py
diff --git a/pydis_site/apps/content/resources/_info.yml b/pydis_site/apps/content/resources/_info.yml
new file mode 100644
index 00000000..6553dcc6
--- /dev/null
+++ b/pydis_site/apps/content/resources/_info.yml
@@ -0,0 +1,2 @@
+title: Pages
+description: Guides, articles, and pages hosted on the site.
diff --git a/pydis_site/apps/content/resources/code-of-conduct.md b/pydis_site/apps/content/resources/code-of-conduct.md
new file mode 100644
index 00000000..6302438e
--- /dev/null
+++ b/pydis_site/apps/content/resources/code-of-conduct.md
@@ -0,0 +1,100 @@
+---
+title: Python Discord Code of Conduct
+description: The Code of Conduct for our community.
+icon: fab fa-discord
+---
+
+# Code of Conduct
+
+We are committed to providing a friendly, safe and welcoming environment for all, regardless of level of experience, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, nationality, or other similar characteristic.
+
+##### Examples of behavior that contributes to creating a positive environment include:
+
+* Being kind and courteous to others
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Collaborating with other community members
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+##### Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and sexual attention or advances
+* The use of inappropriate images, including in a community member's avatar
+* The use of inappropriate language, including in a community member's nickname
+* Any spamming, flaming, baiting or other attention-stealing behavior
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic address, without explicit permission
+* Discussing topics that are overly polarizing, sensitive, or incite arguments. This includes the discussion of polarizing political views, violence, suicide, and rape.
+* Responding with “RTFM”, "just google it” or similar phrases in response to help requests
+* Other conduct which could reasonably be considered inappropriate
+
+### Our Goal
+
+The goal of this document is to set the overall tone for our community.
+This isn’t an exhaustive list of things you can and can't do.
+Rather, take this document in the spirit in which it’s intended, and try to be your best self.
+
+We value many things beyond technical expertise, including collaboration and supporting others within our community.
+Providing a positive experience for other community members can have a much more significant impact than simply providing the correct answer.
+
+### Scope
+
+This Code of Conduct applies to all spaces managed by Python Discord.
+This includes, but is not limited to, the Discord server, our repositories on GitHub, the YouTube-channel, and meet-ups.
+In addition, violations of this code outside these spaces may affect a person's ability to participate within them.
+
+The Python Code of Conduct applies equally to all members of the community, including staff.
+
+---
+
+# Code of Conduct Policies
+
+### Moderation Policy
+
+These are the policies for upholding our community’s rules and the code of conduct.
+If you want to report a situation that needs to be reviewed by our moderation team, please see our [reporting guide](#reporting-guide).
+
+1. The [Python Discord Code of Conduct](#code-of-conduct) and the [Community Rules](/pages/rules) are enforced by the moderation team, which consists of users with the Moderators, Admins or Owners role on the Python Discord server.
+2. Behavior that moderators find inappropriate, whether listed in the code of conduct, the community rules, or not, is also not allowed.
+3. Complaints about moderation in-channel are not allowed. If a moderator takes an action or makes a decision you do not agree with, please send a Direct Message (DM) to ModMail from our Discord server.
+4. If you disagree with a moderation action or decision taken against you, you may appeal the action or decision by following the [Appeal Procedure](#appeal-procedure).
+
+### Reporting Guide
+
+Instances of behaviors that violate the Python Discord Code of Conduct or rules may be reported by any member of the community.
+Community members are encouraged to report these situations, including situations they witness involving other community members.
+
+You may report in the following ways:
+
+* By tagging the `@Moderators` role on the Discord server in situations that require immediate moderator attention.
+* By sending a direct message (DM) to ModMail from our Discord server.
+* By sending an email to [[email protected]](mailto:[email protected])
+
+### Appeal Procedure
+
+If you wish to appeal a decision or action taken by the moderation team, you can do so in one of the following ways:
+
+* By sending an email to [[email protected]](mailto:[email protected])
+* By sending a direct message (DM) to ModMail from our Discord server.
+
+Please provide all relevant information in your appeal, including:
+
+* Your Discord username and id.
+* The decision or action you are appealing.
+* The reason for your appeal.
+
+Appeals will be discussed internally with the moderation team, but will be kept confidential otherwise.
+
+---
+
+#### Attribution
+
+This Code of Conduct and parts of the policies are adapted from the [Adafruit Community Code of Conduct](https://github.com/adafruit/Adafruit_Community_Code_of_Conduct/blob/master/code-of-conduct.md), [Django Code of Conduct](https://www.djangoproject.com/conduct/), and the [Rust Code of Conduct](https://www.rust-lang.org/en-US/conduct.html).
+
+#### License
+![CC-BY](https://licensebuttons.net/l/by/3.0/88x31.png)
+
+All content on this page is licensed under a [Creative Commons Attribution](https://creativecommons.org/licenses/by/3.0/) license.
diff --git a/pydis_site/apps/content/resources/frequently-asked-questions.md b/pydis_site/apps/content/resources/frequently-asked-questions.md
new file mode 100644
index 00000000..ed797532
--- /dev/null
+++ b/pydis_site/apps/content/resources/frequently-asked-questions.md
@@ -0,0 +1,131 @@
+---
+title: Frequently Asked Questions
+description: The Python Discord FAQ.
+icon: fab fa-discord
+toc: 4
+---
+
+Welcome to Python Discord! Sometimes in our channels we get similar questions asked every so often.
+We've compiled the most frequently asked questions and provided our response to them.
+This FAQ is aimed at answering questions about the Python Discord community. If you have question about Python, feel free to ask in `#python-general`.
+
+## Staff & Roles
+
+#### **Q: How do I get the helper role / become moderator / join staff?**
+
+There are no applications to be come a Helper, Moderator, Admin, or other staff role.
+To become a Helper, which is our base staff role, people are nominated by a staff member and are later put up to a vote by Moderators, Admins, and Owners.
+If the candidate received enough votes unanimously, then we offer them the Helper role.
+This whole process takes place in channels only viewable to staff members.
+
+Being a Helper is not only about helping people with Python questions or helping with our projects, but is also about demonstrating an understanding of our community's culture.
+To read more about what we look for in a Helper and to read about our internal staff roles (Moderators, Leads, Core Developer, Admin), check out [this page](/pages/server-info/roles/).
+
+
+#### **Q: What is this role on the server?**
+
+We document the purposes of our most important roles on our website. Check it out [here](/pages/server-info/roles/).
+
+The roles that are not documented on that page are for seasonal events. These are specific to those events and don't impact permissions on the server.
+
+
+#### **Q: What perks are there for nitro boosters? Can I keep the nitro role once the boost expires?**
+
+People who boost our server automatically get a bright cyan role color, but this is purely cosmetic and there aren't any other incentives to give us Nitro boosts.
+Discord itself manages that specific role, so the role is automatically removed when the boost expires.
+
+Patrons who donate via [our Patreon](https://www.patreon.com/python_discord) also get a blue role and this is also a purely cosmetic role.
+We do appreciate our Patreon supporters and our Nitro Boosters!
+
+We have received suggestions to give Nitro boosters non-cosmetic perks like a Nitro boosters lounge or the ability to use emoji reactions in certain channels, though that isn't something we will consider as we don't want to gate any part of the server behind a paywall.
+
+
+#### **Q: I'd like to report inappropriate behavior on the server. How do I do that?**
+
+To report inappropriate or rule-breaking behaviour on the server, please send a direct message to our `@ModMail`.
+You should find the ModMail bot at the top of the server member list.
+If it's an urgent situation that needs immediate moderator attention, such as spam or NSFW content, then you can ping the `@Moderators` role in the server.
+
+## Questions about our bots
+
+#### **Q: Can I get Python bot on my server?**
+
+There isn't a way to invite `@Python` to other servers.
+`@Python` is closely tied to our server architecture and wouldn't be able to properly function without a specific set-up.
+If you are interested in `@Python` though, you can host your own instance of it.
+The entire project is open source and can be found on [our github](https://github.com/python-discord/bot).
+
+#### **Q: What is the Zoot bot?**
+
+Zoot is an instance of [Metricity](https://github.com/python-discord/metricity).
+It collects advanced metrics about the usage of the server.
+Message content is not stored or collected.
+You can view what data we collect in our [data privacy policy](/pages/privacy/).
+
+
+#### **Q: Do any of the bots do X? Can I contribute to the bot?**
+
+We have two bots that provide functionality in our server: `@Python` and `@Sir Lancebot`.
+
+* `@Python` is the bot that helps manage certain server functionality (i.e. our help channel and moderation systems).
+* `@Sir Lancebot` is our community bot that is designed as an entry level project for people to learn about open source contribution.
+* `@Sir Lancebot` contains our fun and silly commands, like `.battleship`, `.BunnyNameGenerator`, `.http_status` which provides dog and cat HTTP status codes, and more!
+
+You can check out [`@Python` here on github](https://github.com/python-discord/bot), and check out [`@Sir Lancebot` here](https://github.com/python-discord/sir-lancebot).
+If you have any questions about how to contribute, drop by the `#dev-contrib` channel in server.
+
+## Server Specific Questions
+
+#### **Q: Why are the help channels named after elements/food?**
+
+We want to keep the help channels uniquely named with it being somewhat easy to remember, so we decided on elements/food. `#help-1` doesn't work as well as `help-carbon` (`help-strawberry`).
+If we had them numbered, they would quickly move out of order and possibly cause confusion for newer members.
+
+
+#### **Q: Why can't I upload a specific file type?**
+
+The only file types that we allow on this server are those that Discord supports a native preview for.
+This is because it's easier and safer for people on the server since they do not need to download a file to view it.
+It's also to ease the burden on our moderators, otherwise they would have to download and check the files posted to the server.
+
+If you want to share code please use our hosted hastebin, [paste.pythondiscord.com](http://paste.pythondiscord.com).
+
+
+#### **Q: Why is this permission not allowed in that channel?**
+
+Our general policy is to allow permissions unless proven that they are negatively affecting the channel.
+If a certain channel doesn't have a permission, chances are it was allowed there at some point, but the cost of moderating or managing it outweighed the benefit.
+Feel free to ask in `#community-meta` if you'd like the reasoning behind any specific decision.
+
+
+#### **Q: Can we have a channel to show off projects or a channel to find people to work on projects with?**
+
+We previously had these channels, though they unfortunately did not work out the way we had hoped.
+Engagement was low and they were a large burden on our moderators due to the number of low quality or rule violating posts.
+
+In general, a real-time chat client isn't the best avenue for showing off your projects or finding collaborators because messages are typically only seen by those actively engaged at the time they are posted.
+You're welcome to showcase your projects in our off-topic channels or on a different platform like Reddit.
+
+
+#### **Q: Can I make a recommendation about a specific feature in the server?**
+
+If you want to make a recommendation or suggestion about the server feel free to post in `#community-meta`.
+You can also open an issue on our meta repo on GitHub, which can be found [here](https://github.com/python-discord/meta).
+
+
+#### **Q: Why did the icon change?**
+
+While we love our blurple Python logo, we also enjoy celebrating other events throughout the year, like Advent of Code, Pride Month, Black History Month, Valentine's Day, Diwali, and more! In the spirit of those celebrations, we like to have some fun and change our icon instead.
+If you're wondering why it's changed this time, check out `#changelog` on the server, as the reasoning for the recent change will be there.
+
+If you'd like to contribute and create a Python Discord server icon for us to use, check out [our branding repo](https://github.com/python-discord/branding) for what we currently have and talk to us in the `#media-branding` channel in the server.
+
+## Misc
+
+#### **Q: Can I interact with the data collected on the server?**
+
+Unfortunately we don't allow direct interaction with the metrics we collect on the server.
+The data we do collect is used for moderation purposes, please see [our Privacy Policy](/pages/privacy/) on what data is collected and how we use it.
+Legend has it that if you say "SQL" or "graphs" enough times, a `@joe` might appear and provide some graphs and run queries you might have in mind.
+
+We do have some public stats available to view here: [https://stats.pythondiscord.com/](https://stats.pythondiscord.com/)
diff --git a/pydis_site/apps/content/resources/guides/_info.yml b/pydis_site/apps/content/resources/guides/_info.yml
new file mode 100644
index 00000000..2f65eaf9
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/_info.yml
@@ -0,0 +1,2 @@
+title: Guides
+description: Made by us, for you.
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/_info.yml b/pydis_site/apps/content/resources/guides/pydis-guides/_info.yml
new file mode 100644
index 00000000..c126a68a
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/_info.yml
@@ -0,0 +1,2 @@
+title: Python Discord Guides
+description: Guides related to the Python Discord server and community.
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/asking-good-questions.md b/pydis_site/apps/content/resources/guides/pydis-guides/asking-good-questions.md
new file mode 100644
index 00000000..971989a9
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/asking-good-questions.md
@@ -0,0 +1,180 @@
+---
+title: Asking Good Questions
+description: A guide for how to ask good questions in our community.
+icon: fab fa-discord
+toc: 3
+---
+
+This document is intended to provide you with the information you need to get help as quickly and effectively as possible.
+If you're stuck on a problem or you just don't understand something, you should always feel welcome to ask.
+
+# Before You Ask
+
+Before you ask your question, there are a few things you can do to find an answer on your own.
+Experienced developers will do the following:
+
+* Read the official documentation for whatever you're working with
+* Use a debugger to inspect your code
+* Examine the traceback when your code raises an exception
+* Do some research online - for example, on Stack Overflow
+* Read the source code for whatever you're working with
+
+Essentially, doing your research is the first step towards a solution to any problem.
+If your problem isn't extremely general, we're going to be doing exactly these steps ourselves when helping you, so doing the legwork beforehand saves everyone a lot of time.
+
+If none of the above steps help you or you're not sure how to do some of the above steps, feel free to ask us for help.
+
+# A Good Question
+
+When you're ready to ask a question, there's a few things you should have to hand before forming a query.
+
+* A code example that illustrates your problem
+* If possible, make this a minimal example rather than an entire application
+* Details on how you attempted to solve the problem on your own
+* Full version information - for example, "Python 3.6.4 with `discord.py 1.0.0a`"
+* The full traceback if your code raises an exception
+* Do not curate the traceback as you may inadvertently exclude information crucial to solving your issue
+
+Your question should be informative, but to the point.
+More importantly, how you phrase your question and how you address those that may help you is crucial.
+Courtesy never hurts, and please type using correctly-spelled and grammatical language as far as you possibly can.
+
+When you're inspecting a problem, don't be quick to assume that you've found a bug, or that your approach is correct.
+While it helps to detail what exactly you're trying to do, you should also be able to give us the bigger picture - describe the goal, not just the step.
+Describe the problem's symptoms in chronological order - not your guesses as to their cause.
+
+| Bad Questions | Good Questions |
+| ------------- | -------------- |
+| Where can I find information on discord.py? | I used Google to try to find more information about "discord.py 1.0.0a", but I couldn't really find anything useful. Does anyone know where I might find a guide to writing commands using this library? |
+| Pillow puts my text at the bottom of the image instead of where I wanted it. Why is it broken? | Pillow appears to insert text at the bottom of the image if the given X coordinate is negative. I had a look at the documentation and searched Stack Overflow, but I couldn't find any information on using negative coordinates to position text. Has anyone attempted this? |
+| I'm having some trouble writing a YouTube random URL generator - can anyone help? | My YouTube random URL generator appears to be returning false positives for tested URLs, stating that a URL points to a real video when that video doesn't actually exist. Obviously there's some issue with how this is checked, but I can't put my finger on it. Is there anything I can check? |
+| I was given this assignment by my teacher, but I'm not sure how to approach it. Does anyone have any ideas? | I have a list of numbers - how do I calculate how many of them are even? Is there a way to remove all the odd numbers from my list? Are there quick ways to find the average of a list of numbers, or add them all together? |
+
+
+# What Not To Ask
+---
+
+#### Q: Can I ask a question?
+
+Yes. Always yes. Just ask.
+
+#### Q: Is anyone here good at Flask / Pygame / PyCharm?
+
+There are two problems with this question:
+
+1. This kind of question does not manage to pique anyone's interest, so you're less likely to get an answer overall.
+ On the other hand, a question like `"Is it possible to get PyCharm to automatically compile SCSS into CSS files"` is much more likely to be interesting to someone.
+ Sometimes, the best answers come from someone who does not already know the answer, but who finds the question interesting enough to go search for the answer on your behalf.
+2. When you qualify your question by first asking if someone is good at something, you are filtering out potential answerers.
+ [Not only are people bad at judging their own skill at something](https://en.wikipedia.org/wiki/Dunning%E2%80%93Kruger_effect), but the truth is that even someone who has zero experience with the framework you're having trouble with might still be of excellent help to you.
+
+So instead of asking if someone is good at something, simply ask your question right away.
+
+#### Q: Can I use `str()` on a `discord.py` Channel object?
+
+Try it yourself and see. Experimentation is a great way to learn, and you'll save a lot of time by just trying things out. Don't be afraid of your computer!
+
+#### Q: My code doesn't work
+
+This isn't a question, and it provides absolutely no context or information.
+Depending on the mood of the people that are around, you may even find yourself ignored.
+Don't be offended by this - just try again with a better question.
+
+#### Q: Can anyone help me break into someone's Facebook account / write a virus / download videos from YouTube?
+
+We will absolutely not help you with hacking, pirating, or any other illegal activity.
+A question like this is likely to be followed up with a ban if the person asking it doesn't back down quickly.
+
+#### Q: Can I send you a private message?
+
+Sure, but keep in mind that our staff members will not provide help via DMs.
+We prefer that questions are answered in a public channel so lurkers can learn from them.
+
+#### Q: Can you help me over Teamviewer?
+
+No, sorry.
+
+
+# Examining Tracebacks
+
+Usually, the first sign of trouble is that when you run your code, it raises an exception.
+For beginning programmers, the traceback that's generated for the exception may feel overwhelming and discouraging at first.
+However, in time, most developers start to appreciate the extensive information contained in the traceback as it helps them track down the error in their code.
+So, don't panic and take a moment to carefully review the information provided to you.
+
+### Reading the Traceback
+
+```py
+Traceback (most recent call last):
+ File "my_python_file.py", line 6, in <module>
+ spam = division(a=10, b=0)
+ File "my_python_file.py", line 2, in division
+ answer = a / b
+ZeroDivisionError: division by zero
+```
+
+In general, the best strategy is to read the traceback from bottom to top.
+As you can see in the example above, the last line of the traceback contains the actual exception that was raised by your code.
+In this case, `ZeroDivisionError: division by zero`, clearly indicates the problem: We're trying to divide by zero somewhere in our code and that obviously can't be right.
+However, while we now know which exception was raised, we still need to trace the exception back to the error in our code.
+
+To do so, we turn to the lines above the exception.
+Reading from bottom to top again, we first encounter the line where the exception was raised: `answer = a / b`.
+Directly above it, we can see that this line of code was `line 2` of the file `my_python_file.py` and that it's in the scope of the function `division`.
+At this point, it's a good idea to inspect the code referenced here to see if we can spot an obvious mistake:
+
+```py
+# Python Code
+1| def division(a, b):
+2| answer = a / b
+3| return answer
+```
+
+Unfortunately, there's no obvious mistake in the code at this point, although one thing we do see here is that this function divides `a` by `b` and that the exception will only occur if `b` is somehow assigned the numeric value `0`.
+
+Keeping that observation in the back of our minds, we continue reading the traceback from bottom to top. The next thing we encounter is `spam = division(a=10, b=0)` from `line 6` of the file `my_python_file.py`.
+In this case, `<module>` tells us that the code is in the global scope of that file.
+While it's already clear from the traceback what's going wrong here, we're passing `b=0` to the function `division`, inspecting the code shows us the same:
+
+```python
+5| spam = division(a=10, b=0)
+6| print(spam)
+```
+
+We have now traced back the exception to a line of code calling the division function with a divisor of `0`.
+Obviously, this is a simplified example, but the exact same steps apply to more complex situations as well.
+
+### The Error is Sometimes in the Line Before the Line in the Traceback
+
+Sometimes, the actual error is in the line just before the one referenced in the traceback.
+This usually happens when we've inadvertently omitted a character meant to close an expression, like a brace, bracket, or parenthesis.
+For instance, the following snippet of code will generate a traceback pointing at the line after the one in which we've missed the closing parenthesis:
+
+```python
+# Python Code
+1| print("Hello, world!"
+2| print("This is my first Python program!")
+
+# Terminal output
+Traceback (most recent call last):
+ File "my_python_file.py", line 2
+ print("This is my first Python program!")
+ ^
+SyntaxError: invalid syntax
+```
+
+The reason this may happen is that Python allows for [implicit line continuation](https://docs.python.org/3/reference/lexical_analysis.html#implicit-line-joining) and will only notice the error when the expression does not continue as expected on the next line.
+So, it's always a good idea to also check the line before the one mentioned in the traceback!
+
+### More Information on Exceptions
+
+Further information on exceptions can be found in the official Python documentation:
+
+* [The built-in exceptions page](https://docs.python.org/3/library/exceptions.html) lists all the built-in exceptions along with a short description of the exception.
+ If you're unsure of the meaning of an exception in your traceback, this is a good place to start.
+* [The errors and exceptions chapter in the official tutorial ](https://docs.python.org/3/tutorial/errors.html) gives an overview of errors and exceptions in Python.
+ Besides explaining what exceptions are, it also explains how to handle expected exceptions graciously to keep your application from crashing when an expected exception is raised and how to define custom exceptions specific to your application.
+
+If you encounter an exception specific to an external module or package, it's usually a good idea to check the documentation of that package to see if the exception is documented.
+Another option is to paste a part of the traceback, usually the last line, into your favorite search engine to see if anyone else has encountered a similar problem.
+More often than not, you will be able to find a solution to your problem this way.
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/code-reviews-primer.md b/pydis_site/apps/content/resources/guides/pydis-guides/code-reviews-primer.md
new file mode 100644
index 00000000..cde7d63e
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/code-reviews-primer.md
@@ -0,0 +1,165 @@
+---
+title: "Code Reviews: A Primer"
+description: An introduction and guide to the world of code reviews!
+icon: fab fa-github
+toc: 3
+---
+
+## The What, the Why, and the How
+
+##### The What & The Why
+
+This is a guide that will cover the unsung hero of the coding process: code reviews.
+Whether you're working professionally or on an open-source project on GitHub, you'll find that code reviews are an essential part of the process.
+
+So what exactly is a code review? The closest thing to compare it to is proofreading.
+In proofreading, you look at an essay someone wrote and give suggestions on how things could be written better or point out mistakes that the writer may have missed.
+Code reviewing is the same way.
+You're given some code that someone wrote and see if you can find ways to improve it or point mistakes that the coder may have missed.
+
+"Hemlock", you might say, "Why should I care? Why would this be important?" The thing to remember is that coding is a team effort.
+Code reviews help the programs we write to be the very best they can be.
+We're all human, and mistakes are made, but through working together, we can correct those mistakes (as best we can) before they get merged into production.
+Even if you're not the best coder in the world, you may spot things that even a professional programmer might miss!
+
+##### The How
+
+And now for the most important part of the guide.
+This will detail the general process of how to write a code review, what to look for, how to do basic testing, and how to submit your review.
+Big thanks to Akarys, who wrote this section of the guide!
+
+---
+
+## Our Code Review Process
+
+> Note: Everything described in this guide is the writer's way of doing code reviews.
+> This is purely informative; there's nothing wrong with branching off of this guide and creating your own method.
+> We would even encourage you to do so!
+
+We usually do reviews in 3 steps:
+
+1. Analyze the issue(s) and the pull request (PR)
+2. Analyze and review the code
+3. Test the functionality
+
+So let's jump right into it!
+
+## Step 1: Analyzing the Issue(s) and the Pull Request
+
+Every issue and pull request has a reason for being.
+While it's possible to do a code review without knowing that reason, it will make your life easier to get the full context of why it exists and what it's trying to do.
+Let's take a look at how we can get that context.
+
+### Analyzing the Issue(s)
+
+In almost every case, the PR will have some linked issues.
+On the right-hand side of the PR's page in GitHub, you'll see a section labeled "Linked issues".
+Reading the issue(s) and their comments will allow you to know why certain decisions were made.
+This will be invaluable when you review the implementation.
+The author of the issue may suggest the code be written one way but the PR ends up writing it another.
+This happens during the development process: New ideas are thought of, optimizations are found, other ideas are removed, etc.
+Even if there are changes, functionality or fix that the issue covers should be covered by the PR.
+
+### Analyzing the PR
+
+The author of the PR will (or should) have made comments detailing how they implemented the code and whether it differs from the proposed implementation in the issue.
+If they do things differently than suggested, they should also have explained why those changes were made.
+
+Remember that just like in the issue, PR comments have value.
+Some of your future concerns about the implementation or even review comments may have already been discussed.
+Although, if you disagree with the decision, feel free to comment about it! The more input we get from our reviewers, the better!
+The last thing you need to remember is that commit messages are important. They're notes from the PR's author that give details as to what they changed. This can help you keep track of what change happened at what point in development, which can help you get some context about a given change.
+
+Now that you know more about the changes, let's start the fun stuff: the code review!
+
+## Step 2: Reviewing the Code Itself
+
+After all, that's why we're here! Let's dive right in!
+
+### The 1st Read: Getting a Sense of the Code
+
+It's impossible to understand the code in the PR your first time through.
+Don't be surprised if you have to read over it a few times in order to get the whole picture.
+When reading the code for the first time, we would recommend you to only try to get a sense of how the code flow.
+What does this mean, you may ask?
+Well, it is about knowing how every individual piece fits together to achieve the desired goal.
+
+Pay close attention to how the functions and classes are called.
+Make sure to read the comments and docstrings; they'll help explain sections of the code that may be confusing.
+Finally, remember that your goal at the moment is to get a general idea of how the code achieves its goal.
+You don't have to understand the finer points at this stage.
+
+### The 2nd Read: Looking at Every Little Detail
+
+Now that you know how the code flows, you can take a closer look at the code.
+Comment on anything you think could be done better.
+This includes, but is not limited to:
+
+* The general structure of the code
+* The variable names
+* The algorithm(s) used
+* The use or the lack of use of already existing code or a library
+* Blocks of code that could benefit from comments
+* Spelling
+* Anything you see that doesn't seem quite right is worth commenting on. Discussing the things you find benefits everyone involved!
+
+Another good technique is to imagine how you would have implemented a specific functionality and compare it with the proposed implementation.
+GitHub has a feature allowing you to mark files as read, and it's recommended to take advantage of it so that you don't lose your place if you take a break.
+Now that you know what to comment on, let's take a closer look at how to comment.
+
+### Leaving Good Review Comments
+
+When leaving a comment, don't forget that we can't know what you're thinking; you have to write it down.
+Your comment should describe why you think this should be changed and how you propose to change it.
+Note that you can omit the latter in some cases if it is outside of your area of expertise for instance.
+There's nothing wrong with using your comments to ask questions! If there's something you're not sure about, don't hesitate to ask in your comment.
+It might indicate that the PR's author needs to add comments, change variable or function names, or even change a block of code entirely.
+
+GitHub has a handy feature that allows you to leave suggestions.
+This means that the author can drop your suggestion into their PR with a click of a button.
+If you provide one, great! It will speed up the review process even more!
+On the opposite side, note that you aren't required to do all of that when leaving a comment. If you are fixing a typo, leaving a suggestion is enough.
+
+If you have concerns about a particular piece of code for example a race condition, it is totally okay to point it out in a comment, even if you don't have a suggested way to fix it.
+
+## Testing the Functionalities
+
+A code review isn't only about looking at the code; it's also about testing it.
+Always try to make sure that the code is working properly before approving the changes.
+
+Something else to note: you are free to review code without testing functionality.
+That's totally okay! Just make sure to mention when you submit your review.
+
+### Reviewing the Functionality
+
+When reviewing functionality, keep asking yourself how you would have designed it and then compare your idea with the implementation.
+Note that you are allowed to give suggestions about how the functionality should be designed.
+
+Start with the basic usages and work your way up to more complicated ones.
+Verify that everything is working as intended.
+If it isn't, leave a comment giving what you've done, what should have happened, and what actually happened, along with any potential logs or errors you could have.
+If you like, you can even try to pinpoint the issue and find a fix for it. We would be very grateful if you did!
+
+### Objective: Break it
+
+Good functionality should be able to handle edge cases.
+Try to throw every edge case you might think of and see how it responds. This might not be needed in some cases, but it's essential for security-related work.
+If the implementation has a security breach, you should absolutely find it! Just like in the previous section, if you find something and comment on it, great!
+If you manage to find a way to fix it and suggest the fix, even better!
+
+### But what if the Project doesn't even start?
+
+This is a tricky one. Sometimes the project won't even start, keeping you from testing it all together.
+
+In this case, you should try to investigate if it is failing because of an error in the code. If it ends up being because of the functionality, you should comment on that.
+
+If you aren't sure why it isn't starting, feel free to ask us!
+
+## Final Words
+
+You did it!
+You are now ready to take on the wild world of code reviewing!
+We know that process can seem long, tedious, and not feel like a necessary task, but it is!
+We are very grateful to you for reading through this and for your potential future code reviews.
+We couldn't move forward without you.
+Thank you!
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing.md
new file mode 100644
index 00000000..f8baa354
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing.md
@@ -0,0 +1,115 @@
+---
+title: Contributing
+description: A guide to contributing to our open source projects.
+icon: fab fa-github
+---
+
+Our projects on Python Discord are open source and [available on Github](https://github.com/python-discord). If you would like to contribute, consider one of the following projects:
+
+<!-- Project cards -->
+<div class="columns is-multiline is-centered is-3 is-variable">
+ <div class="column is-one-third-desktop is-half-tablet">
+ <div class="card github-card">
+ <div class="card-header">
+ <div class="card-header-title is-centered">
+ <a class="is-size-5" href="https://github.com/python-discord/sir-lancebot">
+ <i class="fab fa-github"></i>&ensp;<strong >Sir Lancebot</strong>
+ </a>
+ </div>
+ </div>
+ <div class="card-content">
+ <div class="content">
+ Our community-driven Discord bot.
+ </div>
+ <div class="tags has-addons">
+ <span class="tag is-dark">Difficulty</span>
+ <span class="tag is-primary">Beginner</span>
+ </div>
+ </div>
+ <div class="card-footer">
+ <a href="https://github.com/python-discord/sir-lancebot/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc" class="card-footer-item"><i class="far fa-exclamation-circle"></i>&ensp;Issues</a>
+ <a href="https://github.com/python-discord/sir-lancebot/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc" class="card-footer-item"><i class="fas fa-code-merge"></i>&ensp;PRs</a>
+ </div>
+ <div class="card-footer">
+ <a href="/pages/guides/pydis-guides/contributing/sir-lancebot" class="card-footer-item"><i class="fas fa-cogs"></i>&ensp;Setup and Configuration Guide</a>
+ </div>
+ </div>
+ </div>
+ <div class="column is-one-third-desktop is-half-tablet">
+ <div class="card github-card">
+ <div class="card-header">
+ <div class="card-header-title is-centered">
+ <a href="https://github.com/python-discord/bot">
+ <strong class="is-size-5"><i class="fab fa-github"></i>&ensp;Bot</strong>
+ </a>
+ </div>
+ </div>
+ <div class="card-content">
+ <div class="content">
+ The community and moderation Discord bot.
+ </div>
+ <div class="tags has-addons">
+ <span class="tag is-dark">Difficulty</span>
+ <span class="tag is-warning">Intermediate</span>
+ </div>
+ </div>
+ <div class="card-footer">
+ <a href="https://github.com/python-discord/bot/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc" class="card-footer-item"><i class="far fa-exclamation-circle"></i>&ensp;Issues</a>
+ <a href="https://github.com/python-discord/bot/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc" class="card-footer-item"><i class="fas fa-code-merge"></i>&ensp;PRs</a>
+ </div>
+ <div class="card-footer">
+ <a href="/pages/guides/pydis-guides/contributing/bot" class="card-footer-item"><i class="fas fa-cogs"></i>&ensp;Setup and Configuration Guide</a>
+ </div>
+ </div>
+ </div>
+ <div class="column is-one-third-desktop is-half-tablet">
+ <div class="card github-card">
+ <div class="card-header">
+ <div class="card-header-title is-centered">
+ <a href="https://github.com/python-discord/site">
+ <strong class="is-size-5"><i class="fab fa-github"></i>&ensp;Site</strong>
+ </a>
+ </div>
+ </div>
+ <div class="card-content">
+ <div class="content">
+ The website, subdomains and API.
+ </div>
+ <div class="tags has-addons">
+ <span class="tag is-dark">Difficulty</span>
+ <span class="tag is-danger">Advanced</span>
+ </div>
+ </div>
+ <div class="card-footer">
+ <a href="https://github.com/python-discord/site/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc" class="card-footer-item"><i class="far fa-exclamation-circle"></i>&ensp;Issues</a>
+ <a href="https://github.com/python-discord/site/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc" class="card-footer-item"><i class="fas fa-code-merge"></i>&ensp;PRs</a>
+ </div>
+ <div class="card-footer">
+ <a href="/pages/guides/pydis-guides/contributing/site" class="card-footer-item"><i class="fas fa-cogs"></i>&ensp;Setup and Configuration Guide</a>
+ </div>
+ </div>
+ </div>
+</div>
+
+If you don't understand anything or need clarification, feel free to ask any staff member with the **@PyDis Core Developers** role in the server. We're always happy to help!
+
+### Useful Resources
+
+[Style Guide](./style-guide/) - Information regarding the code styles you should follow when working on our projects.<br>
+[Review Guide](../code-reviews-primer/) - A guide to get you started on doing code reviews.
+
+## Contributors Community
+We are very happy to have many members in our community that contribute to [our open source projects](https://github.com/python-discord/).
+Whether it's writing code, reviewing pull requests, or contributing graphics for our events, it’s great to see so many people being motivated to help out.
+As a token of our appreciation, those who have made significant contributions to our projects will receive a special **@Contributors** role on our server that makes them stand out from other members.
+That way, they can also serve as guides to others who are looking to start contributing to our open source projects or open source in general.
+
+#### Guidelines for the @Contributors Role
+
+One question we get a lot is what the requirements for the **@Contributors** role are.
+As it’s difficult to precisely quantify contributions, we’ve come up with the following guidelines for the role:
+
+- The member has made several significant contributions to our projects.
+- The member has a positive influence in our contributors subcommunity.
+
+The role will be assigned at the discretion of the Admin Team in consultation with the Core Developers Team.
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/_info.yml b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/_info.yml
new file mode 100644
index 00000000..4a338463
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/_info.yml
@@ -0,0 +1,3 @@
+title: Contributing
+description: How to contribute to our open source projects on Github.
+icon: fab fa-github
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/bot.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/bot.md
new file mode 100644
index 00000000..a48b7300
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/bot.md
@@ -0,0 +1,209 @@
+---
+title: Contributing to Bot
+description: A guide to setting up and configuring Bot.
+icon: fab fa-github
+toc: 1
+---
+
+# Requirements
+* [Python 3.8](https://www.python.org/downloads/)
+* [Pipenv](https://github.com/pypa/pipenv#installation)
+ * `pip install pipenv`
+* [Git](https://git-scm.com/downloads)
+ * [Windows](https://git-scm.com/download/win)
+ * [MacOS](https://git-scm.com/download/mac) or `brew install git`
+ * [Linux](https://git-scm.com/download/linux)
+* A running webserver for the [site](../site)
+ * Follow the linked guide only if you don't want to use Docker or if you plan to do development on the site project too.
+
+## Using Docker
+
+Both the site and the bot can be started using Docker.
+Using Docker is generally recommended (but not strictly required) because it abstracts away some additional set up work, especially for the site.
+However, if you plan to attach a debugger to either the site or the bot, run the respective project directly on your system (AKA the _host_) instead.
+
+The requirements for Docker are:
+
+* [Docker CE](https://docs.docker.com/install/)
+* [Docker Compose](https://docs.docker.com/compose/install/) (This already comes bundled on macOS and Windows, so you shouldn't need to install it)
+ * `pip install docker-compose`
+
+---
+# Fork the project
+You will need access to a copy of the git repository of your own that will allow you to edit the code and push your commits to.
+Creating a copy of a repository under your own account is called a _fork_.
+
+* [Learn how to create a fork of the repository here.](../forking-repository)
+
+This is where all your changes and commits will be pushed to, and from where your PRs will originate from.
+
+For any staff member, since you have write permissions already to the original repository, you can just create a feature branch to push your commits to instead.
+
+---
+# Development environment
+1. [Clone your fork to a local project directory](../cloning-repository/)
+2. [Install the project's dependencies](../installing-project-dependencies/)
+3. [Prepare your hosts file (Optional)](../hosts-file/)
+
+---
+# Test server and bot account
+You will need your own test server and bot account on Discord to test your changes to the bot.
+
+* [**Create a test server**](../setting-test-server-and-bot-account#setting-up-a-test-server)
+* [**Create a bot account**](../setting-test-server-and-bot-account#setting-up-a-bot-account)
+* Invite it to the server you just created.
+
+### Privileged Intents
+
+With `discord.py` 1.5 and later, it is now necessary to explicitly request that your Discord bot receives certain gateway events.
+The Python bot requires the `Server Member Intent` to function.
+In order to enable it, visit the [Developer Portal](https://discord.com/developers/applications/) (from where you copied your bot's login token) and scroll down to the `Privileged Gateway Intents` section.
+The `Presence Intent` is not necessary and can be left disabled.
+
+If your bot fails to start with a `PrivilegedIntentsRequired` exception, this indicates that the required intent was not enabled.
+
+### Server Setup
+
+Setup categories, channels, emojis, roles, and webhooks in your server. To see what needs to be added, please refer to the following sections in the `config-default.yml` file:
+
+* `style.emojis`
+* `guild.categories`
+* `guild.channels`
+* `guild.roles`
+* `guild.webhooks`
+
+We understand this is tedious and are working on a better solution for setting up test servers.
+In the meantime, [here](https://discord.new/zmHtscpYN9E3) is a template for you to use.<br>
+
+---
+# Configure the bot
+You will need to copy IDs of the test Discord server, as well as the created channels and roles to paste in the config file.
+If you're not sure how to do this, [check out the information over here.](../setting-test-server-and-bot-account#obtain-the-ids)
+
+1. Create a copy of `config-default.yml` named `config.yml` in the same directory.
+2. Set `guild.id` to your test servers's ID.
+3. Change the IDs in the [sections](#server-setup) mentioned earlier to match the ones in your test server.
+4. Set `urls.site_schema` and `urls.site_api_schema` to `"http://"`.
+5. Set `urls.site`:
+ - If running the webserver in Docker, set it to `"web:8000"`.
+ - If the site container is running separately (i.e. started from a clone of the site repository), then [COMPOSE_PROJECT_NAME](../docker/#compose-project-names) has to be set to use this domain. If you choose not to set it, the domain in the following step can be used instead.
+ - If running the webserver locally and the hosts file has been configured, set it to `"pythondiscord.local:8000"`.
+ - Otherwise, use whatever domain corresponds to the server where the site is being hosted.
+6. Set `urls.site_api` to whatever value you assigned to `urls.site` with `api` prefixed to it, for example if you set `urls.site` to `web:8000` then set `urls.site_api` to `api.web:8000`.
+7. Setup the environment variables listed in the section below.
+
+### Environment variables
+
+These contain various settings used by the bot.
+To learn how to set environment variables, read [this page](../configure-environment-variables) first.
+
+The following is a list of all available environment variables used by the bot:
+
+| Variable | Required | Description |
+| -------- | -------- | -------- |
+| `BOT_TOKEN` | Always | Your Discord bot account's token (see [Test server and bot account](#test-server-and-bot-account)). |
+| `BOT_API_KEY` | When running bot without Docker | Used to authenticate with the site's API. When using Docker to run the bot, this is automatically set. By default, the site will always have the API key shown in the example below. |
+| `REDDIT_CLIENT_ID` | reddit cog | OAuth2 client ID for authenticating with the [reddit API](https://github.com/reddit-archive/reddit/wiki/OAuth2). |
+| `REDDIT_SECRET` | reddit cog | OAuth2 secret for authenticating with the reddit API. *Leave empty if you're not using the reddit API.* |
+| `BOT_SENTRY_DSN` | When connecting the bot to sentry | The DSN of the sentry monitor. |
+| `REDIS_PASSWORD` | When not using FakeRedis | The password to connect to the redis database. *Leave empty if you're not using REDIS.* |
+
+---
+
+If you are running on the host, while not required, we advise you set `use_fakeredis` to `true` in the config file during development to avoid the need of setting up a Redis server.
+It does mean you may lose persistent data on restart but this is non-critical.
+Otherwise, you should set up a Redis instance and fill in the necessary config.
+{: .notification .is-warning }
+
+---
+
+Example `.env` file:
+
+```shell
+BOT_TOKEN=YourDiscordBotTokenHere
+BOT_API_KEY=badbot13m0n8f570f942013fc818f234916ca531
+REDDIT_CLIENT_ID=YourRedditClientIDHere
+REDDIT_SECRET=YourRedditSecretHere
+```
+
+---
+# Run the project
+
+The bot can run with or without Docker.
+When using Docker, the site, which is a prerequisite, can be automatically set up too.
+If you don't use Docker, you have to first follow [the site guide](../site/) to set it up yourself.
+The bot and site can be started using independent methods.
+For example, the site could run with Docker and the bot could run directly on your system (AKA the _host_) or vice versa.
+
+## Run with Docker
+
+The following sections describe how to start either the site, bot, or both using Docker.
+If you are not interested in using Docker, see [this page](../site/) for setting up the site and [this section](#run-on-the-host) for running the bot.
+
+If you get any Docker related errors, reference the [Possible Issues](../docker#possible-issues) section of the Docker page.
+
+### Site and bot
+
+This method will start both the site and the bot using Docker.
+
+Start the containers using Docker Compose while inside the root of the project directory:
+
+```shell
+docker-compose up
+```
+
+The `-d` option can be appended to the command to run in detached mode.
+This runs the containers in the background so the current terminal session is available for use with other things.
+
+### Site only
+
+This method will start only the site using Docker.
+
+```shell
+docker-compose up site
+```
+
+See [this section](#run-on-the-host) for how to start the bot on the host.
+
+### Bot only
+
+This method will start only the bot using Docker.
+The site has to have been started somehow beforehand.
+
+Start the bot using Docker Compose while inside the root of the project directory:
+
+```shell
+docker-compose up --no-deps bot
+```
+
+## Run on the host
+
+Running on the host is particularly useful if you wish to debug the bot.
+The site has to have been started somehow beforehand.
+
+```shell
+pipenv run start
+```
+
+---
+## Working with Git
+Now that you have everything setup, it is finally time to make changes to the bot!
+If you have not yet [read the contributing guidelines](../contributing-guidelines), now is a good time.
+Contributions that do not adhere to the guidelines may be rejected.
+
+Notably, version control of our projects is done using Git and Github.
+It can be intimidating at first, so feel free to ask for any help in the server.
+
+[**Click here to see the basic Git workflow when contributing to one of our projects.**](../working-with-git/)
+
+## Adding new statistics
+
+Details on how to add new statistics can be found on the [statistic infrastructure page](https://blog.pythondiscord.com/statistics-infrastructure).
+We are always open to more statistics so add as many as you can!
+
+## Running tests
+
+[This section](https://github.com/python-discord/bot/blob/main/tests/README.md#tools) of the README in the `tests` repository will explain how to run tests.
+The whole document explains how unittesting works, and how it fits in the context of our project.
+
+Have fun!
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/cloning-repository.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/cloning-repository.md
new file mode 100644
index 00000000..fad54374
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/cloning-repository.md
@@ -0,0 +1,31 @@
+---
+title: Cloning a Repository
+description: A guide to cloning git repositories.
+icon: fab fa-github
+---
+
+> **Note:** The process varies depending on your choice of code editor / IDE, so refer to one of the following guides:
+
+- [Cloning with PyCharm](#cloning-with-pycharm)
+- [Cloning with the command line](#cloning-with-the-command-line)
+
+The following will use the [Sir-Lancebot](https://github.com/python-discord/sir-lancebot/) repository as an example, but the steps are the same for all other repositories. You should have already retrieved your fork's Git URL as described in [**Creating a Fork**](../forking-repository).
+
+---
+
+## Cloning with PyCharm
+1. Load up PyCharm and click `Get from VCS`.<br>
+![Create Project in PyCharm](/static/images/content/contributing/pycharm_create_project.png)
+2. Enter the URL of your forked repository.
+3. Change the directory if you desire and click `Clone`.<br>
+![Clone Git Project in Pycharm](/static/images/content/contributing/pycharm_checkout.png)
+
+---
+
+## Cloning with the command line
+1. Clone your forked repository using `git clone` followed by your fork's Git URL. Then, change your working directory to the repository.
+```shell
+$ git clone https://github.com/<your username>/sir-lancebot
+...
+$ cd sir-lancebot
+```
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/configure-environment-variables.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/configure-environment-variables.md
new file mode 100644
index 00000000..8b8e3f95
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/configure-environment-variables.md
@@ -0,0 +1,23 @@
+---
+title: Configure Environment Variables
+description: A guide to configuring environment variables.
+icon: fas fa-cog
+---
+
+1. Create a text file named **.env** in your project root (that's the base folder of your repository):
+ * Unix/Git Bash: `touch /path/to/project/.env`
+ * Windows CMD: `type nul > \path\to\project\.env` (The error *The system cannot find the file specified* can be safely ignored.)
+> **Note:** The entire file name is literally `.env`
+2. Open the file with any text editor.
+3. Each environment variable is on its own line, with the variable and the value separated by a `=` sign.
+
+Example:
+
+* Set the environment variable `SEASONALBOT_DEBUG` to `True`:
+```
+SEASONALBOT_DEBUG=True
+```
+* Set the environment variable `CHANNEL_ANNOUNCEMENTS` to `12345`:
+```
+CHANNEL_ANNOUNCEMENTS=12345
+```
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines.md
new file mode 100644
index 00000000..aa784a50
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines.md
@@ -0,0 +1,30 @@
+---
+title: Contributing Guidelines
+description: Guidelines to adhere to when contributing to our projects.
+---
+
+Thank you for your interest in our projects!
+
+If you are interested in contributing, **this page contains the golden rules to follow when contributing.**
+Supplemental information [can be found here](./supplemental-information/).
+Do note that failing to comply with our guidelines may lead to a rejection of the contribution.
+
+If you are confused by any of these rules, feel free to ask us in the `#dev-contrib` channel in our [Discord server.](https://discord.gg/python)
+
+# The Golden Rules of Contributing
+
+1. **Lint before you push.** We have simple but strict style rules that are enforced through linting.
+You must always lint your code before committing or pushing.
+[Using tools](./supplemental-information/#linting-and-precommit) such as `precommit` or `black` can make this easier.
+2. **Make great commits.**
+Great commits should be atomic, with a commit message explaining what and why.
+More on that can be found in [this section](./supplemental-information/#writing-good-commit-messages).
+3. **Do not open a pull request if you aren't assigned to the issue.**
+If someone is already working on it, consider offering to collaborate with that person.
+4. **Use assets licensed for public use.**
+Whenever the assets are images, audio or even code, they must have a license compatible with our projects.
+5. **Follow the [Python Discord Code of Conduct](https://pydis.com/coc).**
+We aim to foster a welcoming and friendly environment on our open source projects.
+We take violations of our Code of Conduct very seriously, and may respond with moderator action.
+
+Welcome to our projects!
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines/_info.yml b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines/_info.yml
new file mode 100644
index 00000000..80c8e772
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines/_info.yml
@@ -0,0 +1,2 @@
+title: Contributing Guidelines
+description: Guidelines to adhere to when contributing to our projects.
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines/supplemental-information.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines/supplemental-information.md
new file mode 100644
index 00000000..78a27173
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines/supplemental-information.md
@@ -0,0 +1,97 @@
+---
+title: Supplemental Information
+description: Additional information related to our contributing guidelines.
+---
+
+This page contains additional information concerning a specific part of our development pipeline.
+
+## Writing Good Commit Messages
+
+A well-structured git log is key to a project's maintainability; it provides insight into when and *why* things were done for future maintainers of the project.
+
+Commits should be as narrow in scope as possible.
+Commits that span hundreds of lines across multiple unrelated functions and/or files are very hard for maintainers to follow.
+After about a week they'll probably be hard for you to follow, too.
+
+Please also avoid making minor commits for fixing typos or linting errors.
+*[Don’t forget to lint before you push!](https://soundcloud.com/lemonsaurusrex/lint-before-you-push)*
+
+A more in-depth guide to writing great commit messages can be found in Chris Beam's *[How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/).*
+
+## Code Style
+
+All of our projects have a certain project-wide style that contributions should attempt to maintain consistency with.
+During PR review, it's not unusual for style adjustments to be requested.
+
+[This page](../../style-guide/) will reference the differences between our projects and what is recommended by [PEP 8.](https://www.python.org/dev/peps/pep-0008/)
+
+## Linting and Precommit
+
+On most of our projects, we use `flake8` and `precommit` to ensure that the code style is consistent across the code base.
+
+Running `flake8` will warn you about any potential style errors in your contribution.
+You must always check it **before pushing**.
+Your commit will be rejected by the build server if it fails to lint.
+
+`precommit` is a powerful tool that helps you automatically lint before you commit.
+If the linter complains, the commit is aborted so that you can fix the linting errors before committing again.
+That way, you never commit the problematic code in the first place!
+
+Please refer to the project-specific documentation to see how to setup and run those tools.
+In most cases, it is either `pipenv run [lint | precommit]` or `poetry run [lint | precommit]`.
+
+## Type Hinting
+
+[PEP 484](https://www.python.org/dev/peps/pep-0484/) formally specifies type hints for Python functions, added to the Python Standard Library in version 3.5.
+Type hints are recognized by most modern code editing tools and provide useful insight into both the input and output types of a function, preventing the user from having to go through the codebase to determine these types.
+
+For example:
+
+```python
+import typing
+
+def foo(input_1: int, input_2: typing.Dict[str, str]) -> bool:
+ ...
+```
+
+This tells us that `foo` accepts an `int` and a `dict`, with `str` keys and values, and returns a `bool`.
+
+If the project is running Python 3.9 or above, you can use `dict` instead of `typing.Dict`.
+See [PEP 585](https://www.python.org/dev/peps/pep-0585/) for more information.
+
+All function declarations should be type hinted in code contributed to the PyDis organization.
+
+## Logging
+
+Instead of using `print` statements for logging, we use the built-in [`logging`](https://docs.python.org/3/library/logging.html) module.
+Here is an example usage:
+
+```python
+import logging
+
+log = logging.getLogger(__name__) # Get a logger bound to the module name.
+# This line is usually placed under the import statements at the top of the file.
+
+log.trace("This is a trace log.")
+log.warning("BEEP! This is a warning.")
+log.critical("It is about to go down!")
+```
+
+Print statements should be avoided when possible.
+Our projects currently defines logging levels as follows, from lowest to highest severity:
+
+- **TRACE:** These events should be used to provide a *verbose* trace of every step of a complex process. This is essentially the `logging` equivalent of sprinkling `print` statements throughout the code.
+- **Note:** This is a PyDis-implemented logging level. It may not be available on every project.
+- **DEBUG:** These events should add context to what's happening in a development setup to make it easier to follow what's going while workig on a project. This is in the same vein as **TRACE** logging but at a much lower level of verbosity.
+- **INFO:** These events are normal and don't need direct attention but are worth keeping track of in production, like checking which cogs were loaded during a start-up.
+- **WARNING:** These events are out of the ordinary and should be fixed, but can cause a failure.
+- **ERROR:** These events can cause a failure in a specific part of the application and require urgent attention.
+- **CRITICAL:** These events can cause the whole application to fail and require immediate intervention.
+
+Any logging above the **INFO** level will trigger a [Sentry](http://sentry.io) issue and alert the Core Developer team.
+
+## Draft Pull Requests
+
+Github [provides a PR feature](https://github.blog/2019-02-14-introducing-draft-pull-requests/) that allows the PR author to mark it as a Draft when opening it. This provides both a visual and functional indicator that the contents of the PR are in a draft state and not yet ready for formal review.
+
+This feature should be utilized in place of the traditional method of prepending `[WIP]` to the PR title.
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/docker.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/docker.md
new file mode 100644
index 00000000..63be9f3e
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/docker.md
@@ -0,0 +1,120 @@
+---
+title: Working with Docker & Docker Compose
+description: Guide to running our projects with Docker and Docker CE.
+icon: fab fa-docker
+toc: 2
+---
+
+Both our [Site](../site/) and [Bot](../bot/) projects use Docker and Docker-Compose during development in order to provide an easy to setup and consistent development environment.
+
+Consider reading some of the following topics if you're interested in learning more about Docker itself:
+
+ * [**What is Docker?**](https://docs.docker.com/engine/docker-overview/)
+ * [**How can I learn to use it for my own stuff?**](https://docs.docker.com/get-started/)
+ * [**What about Docker Compose, what's it for?**](https://docs.docker.com/compose/)
+
+# Docker Installation
+You can find installation guides available for your respective OS from the official Docker documentation:
+[https://docs.docker.com/install/](https://docs.docker.com/install/)
+
+## After Installing on Linux
+If you're on Linux, there's a few extra things you should do:
+
+1. [**Add your user to the `docker` user group so you don't have to use `sudo` when running docker or docker-compose.**](#add-user-group)
+2. [**Start up the Docker service.**](#run-the-service)
+3. [**Set the Docker service to start on boot.**](#start-on-boot) **(optional)**
+
+### Run the Service
+Most linux distributions **systemd**, you can start the service with:
+```shell
+$ sudo systemctl start docker
+```
+
+### Add User Group
+```shell
+$ sudo groupadd docker
+$ sudo usermod -aG docker $USER
+```
+Log out and log back in to ensure your group changes work.
+
+### Start on Boot
+```shell
+$ sudo systemctl enable docker
+```
+
+# Possible Issues
+### Couldn't connect to Docker daemon
+```shell
+ERROR: Couldn't connect to Docker daemon at http+docker://localhost - is it running?
+```
+**Problem**<br>
+Your Docker service is either not started, or you haven't yet installed Docker.
+
+**Solution**<br>
+[Start the service](#run-the-service) or ensure it's installed.
+If it's not, [install it](#docker-installation).
+
+### Error loading config file
+```plaintext
+WARNING: Error loading config file: /home/user/.docker/config.json -
+stat /home/user/.docker/config.json: permission denied
+```
+**Problem**<br>
+You initially ran Docker using `sudo` before adding your user to the `docker` group, resulting in your `~/.docker/` directory being created with incorrect permissions.
+
+**Solution**<br>
+Remove the existing `~/.docker/` directory. It will be automatically re-created with the correct permissions.
+
+### Drive has not been shared (Windows users)
+
+When attempting to run the `docker-compose up` command on a Windows machine, you receive the following or similar error message:
+```text
+ERROR: for bot_bot_1 Cannot create container for service bot: b'Drive has not been shared'
+```
+**Problem**<br>
+Windows has not been configured to share drives with Docker.
+
+**Solution**<br>
+> NOTE: Solution requires Windows user credentials for an account that has administrative privileges.
+
+1. Right-click the Docker icon in the Windows system tray, and choose "Settings" from the context menu.<br>
+![Docker Settings](/static/images/content/contributing/docker_settings.png)
+
+2. Click the "Shared Drives" label at the left, and check the box next to the drive letter where your project is stored.<br>
+![Docker Shared Drives](/static/images/content/contributing/docker_shared_drives.png)
+
+3. Click "Apply" and enter appropriate Windows credentials (likely just your own account, if you have administrative privileges).
+
+4. Re-run the `docker-compose up` command.
+
+# Compose Project Names
+When you launch services from a docker-compose, you'll notice the name of the containers aren't just the service name.
+You'll see this when launching your compose, as well as being able to be seen in the command `docker-compose ps` which will list the containers.
+It should match something like this:
+```
+site_site_1
+```
+This matched the following container name format:
+```
+projectname_servicename_1
+```
+By default, your project name will match the name of the folder your project is inside in all lowercase.
+
+You can specify a custom project name by adding a `COMPOSE_PROJECT_NAME` variable to your `.env` file before launching the compose:
+```
+COMPOSE_PROJECT_NAME=site
+```
+Containers with the same project name end up connected to the same network by default.
+For example, the `site` container connects with `postgres` via the matching hostname inside the container.
+Even if you didn't expose a port to the host, the two containers would be able to talk to each other.
+
+You can have two different projects able to communicate in the same way by having them use the same project name.
+We use this feature to allow the `bot` container to communicate with a separate local copy of `site` that may need to be tested during development.
+
+By default, the `bot` container could launch with a name of `bot_bot_1` and the `site` container with a name of `site_site_1`. Since the prefixes are different, they're in distinct projects, so can't talk with each other.
+
+If we got to the bot's `.env` file, and add the line below, we can set `bot` to run in the same project as `site`:
+```
+COMPOSE_PROJECT_NAME=site
+```
+Now they can talk to each other!
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/forking-repository.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/forking-repository.md
new file mode 100644
index 00000000..07535dbe
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/forking-repository.md
@@ -0,0 +1,18 @@
+---
+title: Forking a Repository
+description: A guide to forking git repositories.
+icon: fab fa-github
+---
+
+Before contributing to any project, you will have to fork the project, ie. create your own online copy of the project.
+The following will use the [Sir-Lancebot](https://github.com/python-discord/sir-lancebot/) repository as an example, but the steps are the same for all other repositories.
+
+1. Navigate to the repository page and press the `Fork` button at the top of the page.
+![Github Fork Button](/static/images/content/contributing/fork_button.png)
+2. Fork it to your account.<br>
+![Github Fork to User](/static/images/content/contributing/fork_user.png)
+3. Later, you will need the Git URL of your forked repository in order to clone it.
+In your newly forked repository, copy the Git URL by clicking the green `Code` button, then click the Copy Link button.
+![Github Fork Clone URL](/static/images/content/contributing/fork_clone.png)
+
+> If you have SSH set up with GitHub, you may instead click the `SSH` button above the Copy Link button to get the SSH URL.
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/hosts-file.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/hosts-file.md
new file mode 100644
index 00000000..5d55a7f3
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/hosts-file.md
@@ -0,0 +1,46 @@
+---
+title: Preparing Your Hosts file
+description: How to setup your hosts file for project usage.
+icon: fas fa-cog
+toc: 3
+---
+
+# What's a hosts file?
+The hosts file maps a hostname/domain to an IP address, allowing you to visit a given domain on your browser and have it resolve by your system to the given IP address, even if it's pointed back to your own system or network.
+
+When staging a local [Site](https://pythondiscord.com/pages/contributing/site/) project, you will need to add some entries to your hosts file so you can visit the site with the domain `http://pythondiscord.local`
+
+# What to add
+You would add the following entries to your hosts file.
+
+```plaintext
+127.0.0.1 pythondiscord.local
+127.0.0.1 api.pythondiscord.local
+127.0.0.1 staff.pythondiscord.local
+127.0.0.1 admin.pythondiscord.local
+```
+
+# How to add it
+
+### Linux
+1. Run `sudo nano /etc/hosts`
+2. Enter your user password.
+3. Add the new content at the bottom of the file.
+4. Use `CTRL+X`
+5. Enter `y` to save.
+
+_This covers most linux distributions that come with `nano`, however you're welcome to use whatever CLI text editor you're comfortable with instead._
+
+### Windows
+1. Open Notepad as Administrator.
+2. Open the file `C:\Windows\System32\Drivers\etc\hosts`
+3. Add the new content at the bottom of the file.
+4. Save.
+
+### MacOS
+1. Run `sudo nano /private/etc/hosts` in Terminal.
+2. Enter your user password.
+3. Add the new content at the bottom of the file.
+4. Use `CTRL+X`
+5. Enter `y` to save.
+6. Flush your DNS by running `dscacheutil -flushcache`
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/installing-project-dependencies.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/installing-project-dependencies.md
new file mode 100644
index 00000000..4432236e
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/installing-project-dependencies.md
@@ -0,0 +1,38 @@
+---
+title: Installing Project Dependencies
+description: A guide to installing the dependencies of our projects.
+icon: fab fa-python
+---
+
+> **Note:** The process varies depending on your choice of code editor / IDE, so refer to one of the following guides:
+
+- [Installing dependencies with PyCharm](#installing-dependencies-with-pycharm)
+- [Installing dependencies with the command line](#installing-dependencies-with-the-command-line)
+
+The following will use the [Sir-Lancebot](https://github.com/python-discord/sir-lancebot/) repository as an example, but the steps are the same for all other repositories.
+You should have already cloned your fork as described in [**Cloning a Repository**](../cloning-repository).
+
+---
+
+## Installing dependencies with PyCharm
+1. Load up your project in PyCharm.
+2. Go to the Project Settings by clicking `File`, then `Settings...`. Alternatively, use the shortcut key `Ctrl+Alt+S`.
+3. Navigate to `Project Interpreter`, then click the gear icon and click `Add`.
+![PyCharm Interpreter Settings](/static/images/wiki/contributing/pycharm_interpreter.png)
+4. In the popup window, click `Pipenv Environment`, make sure `Install packages from Pipfile` is checked, then click `OK`.
+![PyCharm Pipenv Environment](/static/images/wiki/contributing/pycharm_pipenv.png)
+5. PyCharm will automatically install the packages required into a virtual environment.
+![PyCharm Project Interpreter](/static/images/wiki/contributing/pycharm_pipenv_success.png)
+
+---
+
+## Installing dependencies with the command line
+1. Make sure you are in the project directory.
+2. Install project and development dependencies:
+```shell
+$ pipenv sync --dev
+```
+* Remember to also set up pre-commit hooks to ensure your pushed commits will never fail linting:
+```shell
+$ pipenv run precommit
+```
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/issues.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/issues.md
new file mode 100644
index 00000000..9151e5e3
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/issues.md
@@ -0,0 +1,121 @@
+---
+title: Issues
+description: Guide to Github issues.
+icon: fab fa-github
+---
+
+## What are Issues?
+
+Issues are tickets that allow us to manage all the suggested features, bugs noticed and discussions about a project.
+
+An Issue ticket should have a simple, easy to understand title and a clearly written description outlining any of the available details.
+Once an Issue is created, people can comment on it and if work is to be actioned due to it, it can be assigned to a contributor so others know that it's being worked on already.
+
+## How do I make an Issue?
+
+**Before making an Issue, search the existing ones!**
+Often, an Issue ticket already exists within the scope of what you might be considering, so be sure to do a search beforehand and if there it, add any new information or suggestions to the comments on the existing Issue instead, or just add a thumbs up if you agree with it.
+
+If you don't see one existing, then:
+
+1. Click the `Issues` tab in a repository:<br>
+![Repository Issues Tab](/static/images/content/contributing/github_issues_tab.png)
+
+2. Click `New Issue`:<br>
+![New Issues Button](/static/images/content/contributing/github_new_issue.png)
+
+3. Enter the title and description for your issue, then click `Submit new issue`:<br>
+![Sample Issue](/static/images/content/contributing/github_sample_issue.png){: width="600" }
+
+## What should I put as a title?
+
+A good title is short and to the point as to what the Issue is about.
+
+Avoid some of the following:
+
+- Writing a long title
+- Being too vague
+- Using informal language
+- Using languages other than English
+
+## What makes a good description?
+
+A good description is well structured and contains all the information available to you at the time of creating it. If additional information comes to light, it can either be added in edits to the description afterwards, or as part of comments.
+
+Try to avoid:
+
+- Masses of unstructured blocks of never ending text
+- Including unnecessary details that aren't constructive to the discussion
+
+Definitely try to:
+
+- Include relevant images in the description if it involves visual aspects
+- Make use of [Github's markdown syntax](https://help.github.com/en/github/writing-on-github/basic-writing-and-formatting-syntax) for text formatting
+
+## What are labels?
+
+Labels allow us to better organise Issues by letting us view what type of Issue it is, how it might impact the codebase and at what stage it's at.
+
+In our repositories, we try to prefix labels belonging to the same group, for example the label groups `status` or `type`. We will be trying to keep to the same general structure across our project repositories, but just have a look at the full lables list in the respective repository to get a clear idea what's available.
+
+If you're a contributor, you can add relevant labels yourself to any new Issue ticket you create.
+
+### General label groups
+
+These label groups should be present on most of our main project repositories and can serve as a guide as to how they're used.
+
+#### area
+Signifies what section of the project/codebase the Issue is focusing on addressing or discussing. Only one area should be selected usually, as the most focused area should be the selected one. Exceptions exist for Issues that impact multiple areas equally, but are generally not the norm.
+
+#### priority
+How urgent the Issue should be addressed:
+
+- `critical` - Super important, likely a bug that's impacting the project severely at this moment.
+- `high` - Important, impacts the project heavily and/or is time sensitive.
+- `normal` - Would be convenient if it's addressed.
+- `low` - Doesn't require us to look at any time soon.
+
+#### status
+Where this issue is at currently:
+
+- `deferred` - Is being put off until a later date
+- `planning` - The Issue is being discussed, implementation is not decided or ready to begin.
+- `stale` - Hasn't been addressed or contributed to in a long time. Worth reconsidering as worth keeping open or bumped in priority if it needs to be done to get it out.
+- `stalled` - Something else has prevented this Issue from moving forward for now.
+- `WIP` - The issue is actively being worked on by someone already.
+
+#### type
+What's the purpose of the Issue:
+
+- `bug` - Addresses a bug in the code
+- `enhancement` - Changes or improves on an existing feature.
+- `feature` - Addresses a possible new feature.
+- `question` - Isn't addressing any code changes, only a discussion or clarification.
+
+#### Non-group labels
+There are 4 labels that aren't in groups as they are globally recognised and shouldn't be renamed:
+
+- `duplicate` - Marks the Issue as being the same or within scope of an existing one
+- `good first issue` - Marks the Issue as being suitable to work on by beginners
+- `help wanted` - More people are needed to work on this Issue
+- `invalid` - Marks the Issue as not being a proper Issue.
+
+## Assignments
+
+Once an Issue is not in the planning/discussing stage and is approved to be worked on, it can be assigned to someone interested in it.
+
+### Can I assign myself?
+
+Only staff can assign themselves to a ticket.
+If a general contributor assigns themself, they'll be unassigned.
+
+### How do I get assigned?
+
+**First check that someone else isn't already assigned.**
+
+Once you're sure it's available and ready to be worked on, you can leave a comment in the Issue ticket.
+Generally, it's first-come first served, so a staff member will usually assign you within the day if they confirm it's clear to do so.
+
+#### Do I get first preference to work on it if I made the Issue ticket?
+As long as you say you'd like to work on it within the description of your ticket or be the first to request so in a comment.
+If you forget to say so and someone else asks to be assigned, we aren't likely to unassign them afterwards, so it's entirely up to the discretion of the other person in that case.
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/setting-test-server-and-bot-account.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/setting-test-server-and-bot-account.md
new file mode 100644
index 00000000..c14fe50d
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/setting-test-server-and-bot-account.md
@@ -0,0 +1,68 @@
+---
+title: Setting Up a Test Server and Bot Account
+description: How to get started with testing our bots.
+icon: fab fa-discord
+---
+
+## Setting up a Test Server
+
+1. Create a Discord Server if you haven't got one already to use for testing.
+
+---
+
+## Setting up a Bot Account
+
+1. Go to the [Discord Developers Portal](https://discordapp.com/developers/applications/).
+2. Click on the `New Application` button, enter your desired bot name, and click `Create`.
+3. In your new application, go to the `Bot` tab, click `Add Bot`, and confirm `Yes, do it!`
+4. Change your bot's `Public Bot` setting off so only you can invite it, save, and then get your **Bot Token** with the `Copy` button.
+> **Note:** **DO NOT** post your bot token anywhere public, or it can and will be compromised.
+5. Save your **Bot Token** somewhere safe to use in the project settings later.
+6. In the `General Information` tab, grab the **Client ID**.
+7. Replace `<CLIENT_ID_HERE>` in the following URL and visit it in the browser to invite your bot to your new test server.
+```plaintext
+https://discordapp.com/api/oauth2/authorize?client_id=<CLIENT_ID_HERE>&permissions=8&scope=bot
+```
+Optionally, you can generate your own invite url in the `OAuth` tab, after selecting `bot` as the scope.
+
+---
+
+## Obtain the IDs
+
+First, enable developer mode in your client so you can easily copy IDs.
+
+1. Go to your `User Settings` and click on the `Appearance` tab.
+2. Under `Advanced`, enable `Developer Mode`.
+
+#### Guild ID
+
+Right click the server icon and click `Copy ID`.
+
+#### Channel ID
+
+Right click a channel name and click `Copy ID`.
+
+#### Role ID
+
+Right click a role and click `Copy ID`.
+The easiest way to do this is by going to the role list in the guild's settings.
+
+#### Emoji ID
+
+Insert the emoji into the Discord text box, then add a backslash `\` right before the emoji and send the message.
+The result should be similar to the following
+
+```plaintext
+<:bbmessage:511950877733552138>
+```
+
+The long number you see, in this case `511950877733552138`, is the emoji's ID.
+
+#### Webhook ID
+
+Once a [webhook](https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks) is created, the ID is found in the penultimate part of the URL.
+For example, in the following URL, `661995360146817053` is the ID of the webhook.
+
+```plaintext
+https://discordapp.com/api/webhooks/661995360146817053/t-9mI2VehOGcPuPS_F8R-6mB258Ob6K7ifhtoxerCvWyM9VEQug-anUr4hCHzdbhzfbz
+```
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot.md
new file mode 100644
index 00000000..4ff98095
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot.md
@@ -0,0 +1,119 @@
+---
+title: Contributing to Sir Lancebot
+description: A guide to setting up and configuring Sir Lancebot.
+icon: fab fa-github
+toc: 1
+---
+
+> Before contributing, please ensure you read the [contributing guidelines](../contributing-guidelines) in full.
+
+---
+# Requirements
+- [Python 3.8](https://www.python.org/downloads/)
+- [Pipenv](https://github.com/pypa/pipenv/blob/master/docs/install.rst#-installing-pipenv)
+- [Git](https://git-scm.com/downloads)
+ - [Windows Installer](https://git-scm.com/download/win)
+ - [MacOS Installer](https://git-scm.com/download/mac) or `brew install git`
+ - [Linux](https://git-scm.com/download/linux)
+
+## Using Docker
+Sir Lancebot can be started using Docker. Using Docker is generally recommended (but not strictly required) because it abstracts away some additional set up work.
+
+The requirements for Docker are:
+
+* [Docker CE](https://docs.docker.com/install/)
+* [Docker Compose](https://docs.docker.com/compose/install/)
+ * `pip install docker-compose`
+ * This is only a required step for linux. Docker comes bundled with docker-compose on Mac OS and Windows.
+
+---
+
+# Fork the Project
+You will need your own remote (online) copy of the project repository, known as a *fork*.
+
+- [**Learn how to create a fork of the repository here.**](../forking-repository)
+
+You will do all your work in the fork rather than directly in the main repository.
+
+---
+
+# Development Environment
+1. Once you have your fork, you will need to [**clone the repository to your computer**](../cloning-repository).
+2. After cloning, proceed to [**install the project's dependencies**](../installing-project-dependencies). (This is not required if using Docker)
+
+---
+# Test Server and Bot Account
+
+You will need your own test server and bot account on Discord to test your changes to the bot.
+
+1. [**Create a test server**](../setting-test-server-and-bot-account#setting-up-a-test-server).
+2. [**Create a bot account**](../setting-test-server-and-bot-account#setting-up-a-bot-account) and invite it to the server you just created.
+3. Create the following text channels:
+ * `#announcements`
+ * `#dev-log`
+ * `#sir-lancebot-commands`
+4. Create the following roles:
+ * `@Admin`
+5. Note down the IDs for your server, as well as any channels and roles created.
+ * [**Learn how to obtain the ID of a server, channel or role here.**](../setting-test-server-and-bot-account#obtain-the-ids)
+
+---
+
+## Environment variables
+You will have to setup environment variables:
+
+* [**Learn how to set environment variables here.**](../configure-environment-variables)
+
+The following variables are needed for running Sir Lancebot:
+
+| Environment Variable | Description |
+| -------- | -------- |
+| `BOT_TOKEN` | Bot Token from the [Discord developer portal](https://discord.com/developers/applications) |
+| `BOT_GUILD` | ID of the Discord Server |
+| `BOT_ADMIN_ROLE_ID` | ID of the role `@Admins` |
+| `ROLE_HELPERS` | ID of the role `@Helpers` |
+| `CHANNEL_ANNOUNCEMENTS` | ID of the `#announcements` channel |
+| `CHANNEL_DEVLOG` | ID of the `#dev-log` channel |
+| `CHANNEL_COMMUNITY_BOT_COMMANDS` | ID of the `#sir-lancebot-commands` channel |
+
+[**Full environment variable reference for this project.**](./env-var-reference)
+
+---
+
+While not required, we advise you set `USE_FAKEREDIS` to `true` in development to avoid the need of setting up a Redis server.
+It does mean you may lose persistent data on restart but this is non-critical.
+Otherwise, please see the below linked guide for Redis related variables.
+{: .notification .is-warning }
+
+---
+# Run the project
+The sections below describe the two ways you can run this project. We recomend Docker as it requires less setup.
+
+## Run with Docker
+Make sure to have Docker running, then use the Docker command `docker-compose up` in the project root.
+The first time you run this command, it may take a few minutes while Docker downloads and installs Sir Lancebot's dependencies.
+
+```shell
+$ docker-compose up
+```
+
+If you get any Docker related errors, reference the [Possible Issues](./docker/possible-issues) section of the Docker page.
+{: .notification .is-warning }
+
+## Run on the host
+After installing project dependencies use the pipenv command `pipenv run start` in the project root.
+
+```shell
+$ pipenv run start
+```
+
+---
+
+# Working with Git
+Now that you have everything setup, it is finally time to make changes to the bot! If you have not yet [read the contributing guidelines](https://github.com/python-discord/sir-lancebot/blob/main/CONTRIBUTING.md), now is a good time. Contributions that do not adhere to the guidelines may be rejected.
+
+Notably, version control of our projects is done using Git and Github. It can be intimidating at first, so feel free to ask for any help in the server.
+
+[**Click here to see the basic Git workflow when contributing to one of our projects.**](../working-with-git/)
+
+Have fun!
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot/_info.yml b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot/_info.yml
new file mode 100644
index 00000000..349e6149
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot/_info.yml
@@ -0,0 +1,2 @@
+title: Contributing to Sir Lancebot
+description: A guide to setting up and configuring Sir Lancebot.
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot/env-var-reference.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot/env-var-reference.md
new file mode 100644
index 00000000..066b703e
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot/env-var-reference.md
@@ -0,0 +1,68 @@
+---
+title: Sir-Lancebot Environment Variable Reference
+description: The full environment variable reference for Sir-Lancebot.
+toc: 2
+---
+## General Variables
+The following variables are needed for running Sir Lancebot:
+
+| Environment Variable | Description |
+| -------- | -------- |
+| `BOT_TOKEN` | Bot Token from the [Discord developer portal](https://discord.com/developers/applications) |
+| `BOT_GUILD` | ID of the Discord Server |
+| `BOT_ADMIN_ROLE_ID` | ID of the role @Admins |
+| `ROLE_HELPERS` | ID of the role @Helpers |
+| `CHANNEL_ANNOUNCEMENTS` | ID of the #announcements channel |
+| `CHANNEL_DEVLOG` | ID of the #dev-log channel |
+| `CHANNEL_COMMUNITY_BOT_COMMANDS` | ID of the #sir-lancebot-commands channel |
+
+---
+## Debug Variables
+Additionally, you may find the following environment variables useful during development:
+
+| Environment Variable | Description |
+| -------- | -------- |
+| `BOT_DEBUG` | Debug mode of the bot | False |
+| `PREFIX` | The bot's invocation prefix | `.` |
+| `CYCLE_FREQUENCY` | Amount of days between cycling server icon | 3 |
+| `MONTH_OVERRIDE` | Interger in range `[0, 12]`, overrides current month w.r.t. seasonal decorators |
+| `REDIS_HOST` | The address to connect to for the Redis database. |
+| `REDIS_PORT` | |
+| `REDIS_PASSWORD` | |
+| `USE_FAKEREDIS` | If the FakeRedis module should be used. Set this to true if you don't have a Redis database setup. |
+| `BOT_SENTRY_DSN` | The DSN of the sentry monitor. |
+| `TRASHCAN_EMOJI` | The emoji to use for the trashcan during paginated embeds |
+
+
+---
+## Tokens/APIs
+If you will be working with an external service, you might have to set one of these tokens:
+
+| Token | Description |
+| -------- | -------- |
+| `GITHUB_TOKEN` | Personal access token for GitHub, raises rate limits from 60 to 5000 requests per hour. |
+| `GIPHY_TOKEN` | Required for API access. [Docs](https://developers.giphy.com/docs/api) |
+| `OMDB_API_KEY` | Required for API access. [Docs](http://www.omdbapi.com/) |
+| `YOUTUBE_API_KEY` | An OAuth Key or Token are required for API access. [Docs](https://developers.google.com/youtube/v3/docs#calling-the-api) |
+| `TMDB_API_KEY` | Required for API access. [Docs](https://developers.themoviedb.org/3/getting-started/introduction) |
+| `NASA_API_KEY` | Required for API access. [Docs](https://api.nasa.gov/) |
+| `IGDB_API_KEY` | Required for API access. A Twitch account is needed. [Docs](https://api-docs.igdb.com/#about) |
+| `WOLFRAM_API_KEY` | |
+| `UNSPLASH_KEY` | Required for API access. Use the `access_token` given by Unsplash. [Docs](https://unsplash.com/documentation) |
+
+---
+## Seasonal Cogs
+These variables might come in handy while working on certain cogs:
+
+| Cog | Environment Variable | Description |
+| -------- | -------- | -------- |
+| Advent of Code | `AOC_LEADERBOARDS` | List of leaderboards seperated by `::`. Each entry should have an `id,session cookie,join code` seperated by commas in that order. |
+| Advent of Code | `AOC_STAFF_LEADERBOARD_ID` | Integer ID of the staff leaderboard. |
+| Advent of Code | `AOC_ROLE_ID` | ID of the advent of code role.
+| Advent of Code | `AOC_IGNORED_DAYS` | Comma seperated list of days to ignore while calulating score. |
+| Advent of Code | `AOC_YEAR` | Debug variable to change the year used for AoC. |
+| Advent of Code | `AOC_CHANNEL_ID` | The ID of the #advent-of-code channel |
+| Advent of Code | `AOC_FALLBACK_SESSION` | |
+| Valentines | `LOVEFEST_ROLE_ID` | |
+| Wolfram | `WOLFRAM_USER_LIMIT_DAY` | |
+| Wolfram | `WOLFRAM_GUILD_LIMIT_DAY` | |
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/site.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/site.md
new file mode 100644
index 00000000..75d27d99
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/site.md
@@ -0,0 +1,144 @@
+---
+title: Contributing to Site
+description: A guide to setting up and configuring Site.
+icon: fab fa-github
+toc: 1
+---
+
+# Requirements
+
+- [Python 3.8](https://www.python.org/downloads/)
+- [Pipenv](https://github.com/pypa/pipenv#installation)
+ - `pip install pipenv`
+- [Git](https://git-scm.com/downloads)
+ - [Windows](https://git-scm.com/download/win)
+ - [MacOS](https://git-scm.com/download/mac) or `brew install git`
+ - [Linux](https://git-scm.com/download/linux)
+
+Using Docker (recommended):
+
+- [Docker CE](https://docs.docker.com/install/)
+- [Docker Compose](https://docs.docker.com/compose/install/)
+ - `pip install docker-compose`
+
+Without Docker:
+
+- [PostgreSQL](https://www.postgresql.org/download/)
+ - Note that if you wish, the webserver can run on the host and still use Docker for PostgreSQL.
+
+---
+# Fork the project
+
+You will need access to a copy of the git repository of your own that will allow you to edit the code and push your commits to.
+Creating a copy of a repository under your own account is called a _fork_.
+
+- [Learn how to create a fork of the repository here.](../forking-repository/)
+
+This is where all your changes and commits will be pushed to, and from where your PRs will originate from.
+
+For any Core Developers, since you have write permissions already to the original repository, you can just create a feature branch to push your commits to instead.
+
+---
+# Development environment
+
+1. [Clone your fork to a local project directory](../cloning-repository/)
+2. [Install the project's dependencies](../installing-project-dependencies/)
+3. [Prepare your hosts file](../hosts-file/)
+
+## Without Docker
+
+Some additional steps are needed when not using Docker. Docker abstracts away these steps which is why using it is generally recommended.
+
+### 1. PostgreSQL setup
+
+Enter psql, a terminal-based front-end to PostgreSQL:
+
+```shell
+psql -qd postgres
+```
+
+Run the following queries to create the user and database:
+
+```sql
+CREATE USER pysite WITH SUPERUSER PASSWORD 'pysite';
+CREATE DATABASE pysite WITH OWNER pysite;
+```
+
+Finally, enter `/q` to exit psql.
+
+### 2. Environment variables
+
+These contain various settings used by the website. To learn how to set environment variables, read [this page](../configure-environment-variables/) first.
+
+```shell
+DATABASE_URL=postgres://pysite:pysite@localhost:7777/pysite
+METRICITY_DB_URL=postgres://pysite:pysite@localhost:7777/metricity
+DEBUG=1
+SECRET_KEY=suitable-for-development-only
+STATIC_ROOT=staticfiles
+```
+
+#### Notes regarding `DATABASE_URL`
+
+- If the database is hosted locally i.e. on the same machine as the webserver, then use `localhost` for the host. Windows and macOS users may need to use the [Docker host IP](../hosts-file/#windows) instead.
+- If the database is running in Docker, use port `7777`. Otherwise, use `5432` as that is the default port used by PostegreSQL.
+- If you configured PostgreSQL in a different manner or you are not hosting it locally, then you will need to determine the correct host and port yourself.
+The user, password, and database name should all still be `pysite` unless you deviated from the setup instructions in the previous section.
+
+---
+# Run the project
+
+The project can be started with Docker or by running it directly on your system.
+
+## Run with Docker
+
+Start the containers using Docker Compose:
+
+```shell
+docker-compose up
+```
+
+The `-d` option can be appended to the command to run in detached mode. This runs the containers in the background so the current terminal session is available for use with other things.
+
+If you get any Docker related errors, reference the [Possible Issues](https://pythondiscord.com/pages/contributing/docker/#possible-issues") section of the Docker page.
+{: .notification .is-warning }
+
+## Run on the host
+
+Running on the host is particularily useful if you wish to debug the site. The [environment variables](#2-environment-variables) shown in a previous section need to have been configured.
+
+### Database
+
+First, start the PostgreSQL database.
+Note that this can still be done with Docker even if the webserver will be running on the host - simply adjust the `DATABASE_URL` environment variable accordingly.
+
+If you chose to use Docker for just the database, use Docker Compose to start the container:
+
+```shell
+docker-compose up postgres
+```
+
+If you're not using Docker, then use [pg_ctl](https://www.postgresql.org/docs/current/app-pg-ctl.html) or your system's service manager if PostgreSQL isn't already running.
+
+### Webserver
+
+Starting the webserver is done simply through pipenv:
+
+```shell
+pipenv run start
+```
+
+---
+# Working on the project
+
+The development environment will watch for code changes in your project directory and will restart the server when a module has been edited automatically.
+Unless you are editing the Dockerfile or docker-compose.yml, you shouldn't need to manually restart the container during a developing session.
+
+[**Click here to see the basic Git workflow when contributing to one of our projects.**](../working-with-git/)
+
+---
+# Django admin site
+
+Django provides an interface for administration with which you can view and edit the models among other things.
+
+It can be found at [http://admin.pythondiscord.local:8000](http://admin.pythondiscord.local:8000). The default credentials are `admin` for the username and `admin` for the password.
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/style-guide.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/style-guide.md
new file mode 100644
index 00000000..f9962990
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/style-guide.md
@@ -0,0 +1,211 @@
+---
+title: Style Guide
+description: Coding conventions for our Open Source projects.
+icon: fab fa-python
+---
+
+> A style guide is about consistency.
+> Consistency with this style guide is important.
+> Consistency within a project is more important.
+> Consistency within one module or function is the most important.
+
+> However, know when to be inconsistent -- sometimes style guide recommendations just aren't applicable.
+> When in doubt, use your best judgment. Look at other examples and decide what looks best. And don't hesitate to ask!
+
+> — [PEP 8, the general Style Guide for Python Code](https://www.python.org/dev/peps/pep-0008/)
+
+All of our projects have a certain project-wide style that contributions should attempt to maintain consistency with.
+During PR review, it's not unusual for style adjustment requests to be commented.
+
+We've added below a guideline to aid new contributors, allowing them to refer to it during development, to help get more familiar and to hopefully lessen some of the frustrations that come from first-time contributions.
+
+Anything that isn't defined below falls back onto the [PEP 8 guidelines](https://www.python.org/dev/peps/pep-0008/), so be sure to reference it also.
+
+# Code Structure
+## Maximum Line Length
+Each project has specified their respective maximum line lengths.
+Generally, we try to keep this at 100 or 120 characters, making our length longer than the typical 79 characters.
+
+Most IDEs and smarter editors will use the lint settings we store in the project's `tox.ini` or `.flake8` file after you install the appropriate development packages, so should conflict with our suggested project rules.
+If your editor does not have this ability but instead requires setting it manually, make sure to change it to the appropriate length specified in these files.
+
+## Line Breaks
+Avoid breaking a line far earlier than necessary, such as:
+
+```py
+array = [ # there was plenty of room on this line
+ 1, 2, 3,
+ 4, 5, 6
+]
+```
+
+Try instead to make use of the space you're allowed to use appropriately:
+```py
+array = [1, 2, 3, 4, 5, 6]
+```
+
+Any line continuations must be indented a full level, i.e. 4 spaces. So don't do:
+```py
+def an_example_function_definition_that_is_kinda_long(
+ variable_name_of_the_first_positional_argument, # only 2 spaces on the indent
+ variable_name_of_the_second_positional_argument # same here
+)
+```
+
+Do instead:
+```py
+def an_example_function_definition_that_is_kinda_long(
+ variable_name_of_the_first_positional_argument,
+ variable_name_of_the_second_positional_argument
+)
+```
+
+### Bracket and Item Arrangement
+In the case where items contained in brackets need to be broken across multiple lines, items should be dropped to a new line after the opening bracket with an additional level of indentation.
+The closing bracket ends on it's own new line, on the same indentation level as the opening bracket.
+
+Avoid doing:
+```py
+def long_function_name_that_is_taking_up_too_much_space(var_one, var_two, var_three, # didn't drop a line after the brackets
+ var_four, var_five, var_six,
+ var_seven, var_eight):
+ print(var_one)
+```
+```py
+def long_function_name_that_is_taking_up_too_much_space(
+ var_one,
+ var_two,
+ var_three,
+ var_four,
+ var_five,
+ var_six,
+ var_seven,
+ var_eight): # didn't drop the closing bracket to a new line
+ print(var_one)
+```
+
+Instead the correct style is:
+```py
+def long_function_name_that_is_taking_up_too_much_space(
+ var_one,
+ var_two,
+ var_three,
+ var_four,
+ var_five,
+ var_six,
+ var_seven,
+ var_eight
+):
+ print(var_one)
+```
+
+## Imports
+Our projects require correctly ordering imports based on the pycharm import order rules.
+If you use Pycharm as your main IDE, you can also use the `CTRL+ALT+O` shortcut to automatically reorder your imports to the correct style.
+
+There's three groups of imports which are defined in the following order:
+
+- Standard library
+- 3rd party
+- Local
+
+Each group must be ordered alphabetically, with uppercase modules coming before lowercase.
+```py
+from packagename import A, Z, c, e
+```
+
+Direct imports must be distinct, so you cannot do:
+```py
+import os, sys
+```
+Instead do:
+```py
+import os
+import sys
+```
+
+Absolute referencing for local project modules are preferenced over relative imports.
+
+Wildcard imports should be avoided.
+
+# Strings
+## Quote Marks
+Preference is to use double-quotes (`"`) wherever possible.
+Single quotes should only be used for cases where it is logical.
+Exceptions might include:
+
+- using a key string within an f-string: `f"Today is {data['day']}"`.
+- using double quotes within a string: `'She said "oh dear" in response'`
+
+Docstrings must use triple double quotes (`"""`).
+
+## Docstrings
+All public methods and functions should have docstrings defined.
+
+### Line Structure
+Single-line docstrings can have the quotes on the same line:
+```py
+def add(a, b):
+ """Add two arguments together."""
+ return a + b
+```
+
+Docstrings that require multiple lines instead keep both sets of triple quotes on their own lines:
+```py
+def exponent(base, exponent=2):
+ """
+ Calculate the base raised to the exponents power.
+
+ Default is 2 due to a squared base being the most common usage at this time.
+ """
+ return a ** b
+```
+
+### Spacing
+Functions and methods should not have an extra empty newline after the docstring.
+```py
+def greeting(name):
+ """Build a greeting string using the given name."""
+ return f"Welcome, {name}"
+```
+
+Class docstrings do require an extra newline.
+```py
+class SecretStuffCog(commands.Cog):
+ """Handle the secret commands that must never been known."""
+
+ def __init__(self, bot):
+ ...
+```
+
+### Mood
+Imperative mood and present tense usage is preferenced when writing docstrings.
+
+Imperative mood is a certain grammatical form of writing that expresses a clear command to do something.
+
+**Use:** "Build an information embed."<br>
+**Don't use:** "Returns an embed containing information."
+
+Present tense defines that the work being done is now, in the present, rather than in the past or future.
+
+**Use:** "Build an information embed."<br>
+**Don't use:** "Built an information embed." or "Will build an information embed."
+
+# Type Annotations
+Functions are required to have type annotations as per the style defined in [PEP 484](https://www.python.org/dev/peps/pep-0484/).
+
+A function without annotations might look like:
+```py
+def divide(a, b):
+ """Divide the two given arguments."""
+ return a / b
+```
+
+With annotations, the arguments and the function are annotated with their respective types:
+```py
+def divide(a: int, b: int) -> float:
+ """Divide the two given arguments."""
+ return a / b
+```
+
+In previous examples, we have purposely omitted annotations to keep focus on the specific points they represent.
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git.md
new file mode 100644
index 00000000..26c89b56
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git.md
@@ -0,0 +1,23 @@
+---
+title: Working with Git
+description: Basic workflows when using git.
+icon: fab fa-git-alt
+---
+
+Working with git can be daunting, but it is a powerful tool for collaboration and version control.
+Below are links to regular workflows for working with Git using PyCharm or the CLI.
+
+> **What's the difference?**<br>
+> The integrated Git tool built into PyCharm offers a more visual and abstract way to use Git to manage your files.<br>
+> However, the CLI offers more minute control and functionality compared to the GUI, which may not always do exactly what you want.
+
+* [**Working with Git in PyCharm**](./pycharm)
+* [**Working with the Git CLI**](./cli)
+
+---
+
+**Resources to learn Git**
+
+* [The Git Book](https://git-scm.com/book)
+* [Corey Schafer's Youtube Tutorials](https://www.youtube.com/watch?v=HVsySz-h9r4&list=PL-osiE80TeTuRUfjRe54Eea17-YfnOOAx)
+* [GitHub Git Resources Portal](https://try.github.io/)
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/_info.yml b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/_info.yml
new file mode 100644
index 00000000..68ef3fd6
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/_info.yml
@@ -0,0 +1,3 @@
+title: Working with Git
+description: Basic workflows when using git.
+icon: fab fa-git-alt
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/cli.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/cli.md
new file mode 100644
index 00000000..5f196837
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/cli.md
@@ -0,0 +1,121 @@
+---
+title: Working with the Git CLI
+description: Basic workflow when using the git CLI.
+toc: 2
+---
+
+This is the basic workflow when working with Git with CLI. For the PyCharm version of the guide, [**click here**](../pycharm).
+The following will use the [Sir-Lancebot](https://github.com/python-discord/sir-lancebot/) repository as an example, but the steps are the same for all other repositories.
+
+> **Note:** This is a guide only meant to get you started with git. For in-depth resources, check the [**Working with Git**](..) page.
+
+---
+
+## Adding the Upstream Remote
+Adding a *remote* to the main GitHub repository you forked off will allow you to later update your fork with changes from the main repository.
+
+Generally, a *remote* designates a repository that is on GitHub or another external location rather than on your computer.
+The `origin` remote will refer to your fork on GitHub. The `upstream` remote will refer to the main repository on GitHub.
+```sh
+$ git remote add upstream https://github.com/python-discord/sir-lancebot.git
+```
+If you use SSH, use `[email protected]:python-discord/sir-lancebot.git` for the upstream URL instead.
+
+---
+
+## Creating a New Branch
+You will be committing your changes to a new branch rather than to `main`.
+Using branches allows you to work on muiltiple pull requests without conflicts.
+
+You can name your branch whatever you want, but it's recommended to name it something succint and relevant to the changes you will be making.
+
+Run the following commands to create a new branch. Replace `branch_name` with the name you wish to give your branch.
+```sh
+$ git fetch --all
+...
+$ git checkout --no-track -b branch_name upstream/main
+```
+
+---
+
+## Staging Changes
+Files in git can be in one of four different states:
+
+- *Staged*: These files have been modified and will be committed.
+- *Unstaged*: These files were already present but have been modified.
+- *Untracked*: These files are new to the repository.
+- *Ignored*: Specified in a `.gitignore` file in the project root, these files will never be committed, remaining only on your computer.
+
+As you can see, only staged files will end up being committed.
+You can get an overview of this using `git status`.
+If you wish to commit unstaged or untracked files, you will need to add them with `git add` first.
+```sh
+# Add files individually
+$ git add path/to/file.py path/to/other/file.py
+
+# Add all unstaged and untracked files in a directory
+$ git add path/to/directory
+
+# Add all unstaged and untracked files in the project
+$ git add .
+
+# Add all tracked and modified files in the project
+$ git add -u
+
+# Unstage a file
+$ git reset -- path/to/file.py
+```
+
+---
+
+## Discarding Changes
+Be careful, these operations are **irreversible**!
+```sh
+# Discard changes to an unstaged file
+$ git checkout -- path/to/file.py
+
+# Discard ALL uncommitted changes
+$ git reset --hard HEAD
+```
+
+---
+
+## Committing Changes
+The basic command for committing staged changes is `git commit`. All commits must have a message attached to them.
+```sh
+# Commit staged changes and open your default editor to write the commit message
+$ git commit
+
+# Specify the message directly
+$ git commit -m "Turn pride avatar into an embed"
+
+# Commit all staged and unstaged changes. This will NOT commit untracked files
+$ git commit -a -m "Update d.py documentation link"
+```
+
+---
+
+## Pushing Commits
+Commits remain local (ie. only on your computer) until they are pushed to the remote repository (ie. GitHub).
+
+The first time you push on your new branch, you'll need to set the upstream when you push:
+```sh
+$ git push -u origin branch_name
+```
+Any subsequent pushes can be done with just `git push`.
+
+---
+
+## Pulling Changes
+Sometimes you want to update your repository with changes from GitHub.
+This could be the case if you were working on the pull request on two different computers and one of them has an outdated local repository.
+
+You can pull the changes from GitHub with:
+```sh
+$ git pull
+```
+You can also pull changes from other branches such as from branch `main` in `upstream`:
+```sh
+$ git pull upstream main
+```
+This should generally only be needed if there are [merge conflicts](https://help.github.com/en/articles/about-merge-conflicts) that you need to resolve manually. Conflicts arise when you change the same code that someone else has changed and pushed since you last updated your local repository.
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/pycharm.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/pycharm.md
new file mode 100644
index 00000000..3f7fefa0
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/pycharm.md
@@ -0,0 +1,67 @@
+---
+title: Working with Git in PyCharm
+description: Basic workflow when using git in PyCharm.
+toc: 2
+---
+
+This is the basic workflow when working with Git with PyCharm. For the CLI version of the guide, [**click here**](../cli).
+The following will use the [Sir-Lancebot](https://github.com/python-discord/sir-lancebot/) repository as an example, but the steps are the same for all other repositories.
+
+> **Note:** This is a guide only meant to get you started with git. For in-depth resources, check the [**Working with Git**](wiki:/contributing/working-with-git/) page.
+
+---
+
+## Adding the Upstream Remote
+> Adding a *remote* to the main GitHub repository you forked off will allow you to later update your fork with changes from the main repository.
+
+> Generally, a *remote* designates a repository that is on GitHub or another external location rather than on your computer. The `origin` remote will refer to your fork on GitHub. The `upstream` remote will refer to the main repository on GitHub.
+
+1. In the menu bar, navigate to `Git` -> `Remotes...`.<br>
+![PyCharm Remotes](/static/images/content/contributing/pycharm_remotes.png)
+2. In the popup menu, click the `+` icon, set `upstream` as the name, set the URL as the URL for the main repository on GitHub.<br>
+![PyCharm Upstream Project](/static/images/content/contributing/pycharm_upstream.png)
+3. Click `OK`.
+
+---
+
+## Creating a New Branch
+> You will be committing your changes to a new branch rather than to `main`. Using branches allows you to work on multiple pull requests at the same time without conflicts.
+
+> You can name your branch whatever you want, but it's recommended to name it something succint and relevant to the changes you will be making.
+
+> Before making new branches, be sure to checkout the `main` branch and ensure it's up to date.
+
+1. In the bottom right corner, click on `main` and then click `New Branch`.<br>
+![PyCharm New Branch](/static/images/content/contributing/pycharm_branch.png)
+
+---
+
+## Committing Changes
+After making changes to the project files, you can commit by clicking the commit button that's part of the Git actions available in the top right corner of your workspace:
+
+![PyCharm Commit Button](/static/images/content/contributing/pycharm_commit_button.png)
+
+The flow of making a commit is as follows:
+
+1. Select the files you wish to commit.
+2. Write a brief description of what your commit is. This is your *commit message*.
+3. See the actual changes your commit will be making, and optionally tick/untick specific changes to only commit the changes you want.
+4. Click `Commit`.<br>
+![PyCharm Commit](/static/images/content/contributing/pycharm_commit.png)
+
+---
+
+## Pushing Changes
+When you are ready to have your commits be available in your remote fork, navigate to `Git` -> `Push...`.
+Select the commits you want to push, make sure the remote branch is your intended branch to push to, and click `Push`.
+
+![PyCharm Push](/static/images/content/contributing/pycharm_push.png)
+
+---
+
+## Pulling Changes
+> Sometimes you want to update your repository with changes from GitHub. This could be the case if you were working on the pull request on two different computers and one of them has an outdated local repository.
+
+To do that, navigate to `Git` -> `Pull...`. From there, select the *remote* and the branches to pull from, then click `Pull`.
+
+![PyCharm Pull](/static/images/content/contributing/pycharm_pull.png)
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/help-channel-guide.md b/pydis_site/apps/content/resources/guides/pydis-guides/help-channel-guide.md
new file mode 100644
index 00000000..2a6e7781
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/help-channel-guide.md
@@ -0,0 +1,78 @@
+---
+title: Help Channels
+description: How do help channels work in the Python Discord community?
+icon: fab fa-discord
+relevant_links:
+ Asking Good Questions: ../asking-good-questions
+ Role Guide: /pages/server-info/roles
+ Helping Others: ../helping-others
+---
+
+On the 5th of April 2020, we introduced a new help channel system at Python Discord. This article is a supplementary guide to explain precisely where to go to find help.
+
+We have two different kinds of help channels in our community - **Topical help channels**, and **general help channels**.
+Where you should go depends on what you need help with.
+These channels also attract different helpers, and move at different speeds, which affects the kind of help you're likely to receive, and how fast you get that help.
+
+# Topical Help Channels
+
+The topical help channels move at a slower pace than the general help channels.
+They also sometimes attract domain experts - for example, `#async-and-concurrency` has CPython contributors who helped write asyncio, and in `#game-development` you can find the creators and maintainers of several game frameworks.
+If your question fits into the domain of one of our topical help channels, and if you're not in a big hurry, then this is probably the best place to ask for help.
+
+![List of topical help channels](/static/images/content/help_channels/topical_channels.png)
+
+Some of the topical help channels have a broad scope, so they can cover many (somewhat) related topics.
+For example, `#data-science-and-ai` covers scientific Python, statistics, and machine learning, while `#algos-and-data-structs` covers everything from data structures and algorithms to maths.
+
+To help you navigate this, we've added a list of suggested topics in the topic of every channel.
+If you're not sure where to post, feel free to ask us which channel is relevant for a topic in `#community-meta`.
+
+# General Help Channels
+
+Our general help channels move at a fast pace, and attract a far more diverse spectrum of helpers.
+This is a great choice for a generic Python question, and a good choice if you need an answer as soon as possible.
+It's particularly important to [ask good questions](..guides/asking-good-questions) when asking in these channels, or you risk not getting an answer and having your help channel be claimed by someone else.
+
+## How To Claim a Channel
+
+There are always 3 available help channels waiting to be claimed in the **Python Help: Available** category.
+
+![Available help channels](/static/images/content/help_channels/available_channels.png)
+
+In order to claim one, simply start typing your question into one of these channels. Once your question has been posted, you have claimed this channel, and the channel will be moved down to the **Python Help: Occupied** category.
+
+If you're unable to type into these channels, this means you're currently **on cooldown**. In order to prevent someone from claiming all the channels for themselves, **we only allow someone to claim a new help channel every 15 minutes**. However, if you close your help channel using the `!dormant` command, this cooldown is reset early.
+
+![Channel available message](/static/images/content/help_channels/available_message.png)
+*This message is always posted when a channel becomes available for use.*
+
+## Q: For how long is the channel mine?
+
+The channel is yours until it has been inactive for **30 minutes**. When this happens, we move the channel down to the **Python Help: Dormant** category, and make the channel read-only. After a while, the channel will be rotated back into **Python Help: Available** for the next question. Please try to resist the urge to continue bumping the channel so that it never gets marked as inactive. If nobody is answering your question, you should try to reformulate the question to increase your chances of getting help.
+
+![Channel dormant message](/static/images/content/help_channels/dormant_message.png)
+*You'll see this message in your channel when the channel is marked as inactive.*
+
+## Q: I don't need my help channel anymore, my question was answered. What do I do?
+
+Once you have finished with your help channel you or a staff member can run `!dormant`. This will move the channel to the **Python Help: Dormant** category where it will sit until it is returned to circulation. You will only be able to run the command if you claimed the channel from the available category, you cannot close channels belonging to others.
+
+## Q: Are only Helpers supposed to answer questions?
+
+Absolutely not. We strongly encourage all members of the community to help answer questions. If you'd like to help answer some questions, simply head over to one of the help channels that are currently in use. These can be found in the **Python Help: Occupied** category.
+
+![Occupied help channels](/static/images/content/help_channels/occupied_channels.png)
+
+Anyone can type in these channels, and users who are particularly helpful [may be offered a chance to join the staff on Python Discord](/pages/server-info/roles/#note-regarding-staff-roles).
+
+## Q: I lost my help channel!
+
+No need to panic.
+Your channel was probably just marked as dormant.
+All the dormant help channels are still available at the bottom of the channel list, in the **Python Help: Dormant** category, and also through search.
+If you're not sure what the name of your help channel was, you can easily find it by using the Discord Search feature.
+Try searching for `from:<your nickname>` to find the last messages sent by yourself, and from there you will be able to jump directly into the channel by pressing the Jump button on your message.
+
+![Dormant help channels](/static/images/content/help_channels/dormant_channels.png)
+*The dormant help channels can be found at the bottom of the channel list.*
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/helping-others.md b/pydis_site/apps/content/resources/guides/pydis-guides/helping-others.md
new file mode 100644
index 00000000..d126707d
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/helping-others.md
@@ -0,0 +1,140 @@
+---
+title: Helping Others
+description: The staff's take on how to help others in our community.
+icon: fab fa-discord
+relevant_links:
+ Asking Good Questions: ../asking-good-questions
+ Help Channel Guide: ../help-channel-guide
+ Code of Conduct: /pages/code-of-conduct/
+toc: 2
+---
+
+Python Discord has a lot of people asking questions, be it in the help channels, topical channels, or any other part of the server.
+Therefore, you might sometimes want to give people the answers you have in mind.
+But you might not be sure how best to approach the issue, or maybe you'd like to see how others handle it.
+This article aims to present a few of the general principles which guide the staff on a day-to-day basis on the server.
+
+## Understanding the Problem
+
+Some people are good at asking for help.
+They might be able to present their problem accurately and concisely, in which case discussing the problem and giving tips is simple and straight-forward.
+But not everyone might be able to do that for their current issue.
+Maybe they just don't know what they don't know.
+
+If you feel like there's a gap in your understanding of the problem, it's often a good first step to query the asker for more information. Some of this information might be:
+
+* More code
+* The way in which the code doesn't work (if that is the issue), be it an exception message with traceback, undesired output, etc.
+* You can sometimes infer what the problem is yourself by requesting short examples of the desired output for specific input.
+
+At this point, it's probably better being safe than sorry.
+You don't want to accidentally pursue a direction that isn't even related to the real issue, as it could lead to frustration on both sides.
+Beginners especially can be prone to asking a question which presents their attempt at solving the problem, instead of presenting the problem itself.
+This is often called an [XY problem](https://xyproblem.info/).
+
+Even if eventually you can't help, simply clarifying what the problem is can help others join in, and give their input.
+
+> #### Example 1:
+> A person might ask: *"How do I look at the values inside my function from outside?"*
+>
+> What they might be asking is: *"How do I return a value from a function?"*
+
+
+## Understanding the Helpee
+
+Assuming you know what the problem is, it's vital to gauge the level of knowledge of the person asking the question.
+There's a stark difference between explaining how to do something to someone experienced, who only lacks knowledge on the specific topic; and someone who is still learning the basics of the language.
+
+Try adjusting the solutions you present and their complexity accordingly.
+Teaching new concepts allows the helpee to learn, but presenting too complex of a solution might overwhelm them, and the help session might not achieve its purpose.
+
+> #### Example 2:
+> A user might ask how to count how often each word appears in a given text.
+> You might lean towards solving it using `collections.Counter`, but is it really the right approach?
+> If the user doesn't know yet how to update and access a dictionary, it might be better to start there.
+
+Generally, you should consider what approach will bring the most value to the person seeking help, instead of what is the most optimal, or "right" solution.
+
+Usually, understanding a person's level can be achieved after a short conversation (such as trying to understand the problem), or simply by seeing the person's code and what they need help with.
+At other times, it might not be as obvious, and it might be a good idea to kindly inquire about the person's experience with the language and programming in general.
+
+
+## Teach a Man to Fish...
+
+The path is often more important than the answer.
+Your goal should primarily be to allow the helpee to apply, at least to a degree, the concepts you introduce in your answer.
+Otherwise, they might keep struggling with the same problem over and over again.
+That means that simply showing your answer might close the help channel for the moment, but won't be very helpful in the long-term.
+
+A common approach is to walk the helpee through to an answer:
+
+* Break the task into smaller parts that will be easier to handle, and present them step by step.
+ Try to think of the order of the steps you yourself would take to reach the solution, and the concepts they need to understand for each of those steps.
+ If one step requires the helpee to understand several new concepts, break it down further.
+* Ask instructive questions that might help the person think in the right direction.
+
+> #### Example 3:
+>
+> **user**: "Hey, how can I create a sudoku solver?"<br>
+> *helper1 proceeds to paste 40 lines of sudoku solving code*<br>
+> **helper2**: "Are you familiar with lists / recursion / backtracking?"<br>
+> *helper2 proceeds to give the information the user lacks*
+>
+> With the first replier, there's a much smaller chance of the helpee understanding how the problem was solved, and gaining new tools for future projects.
+> It's much more helpful in the long run to explain the new concepts or guide them to resources where they can learn.
+>
+> This is also an example of gauging the level of the person you're talking to.
+> You can't properly help if you don't know what they already learned.
+> If they don't know recursion, it's going to take a slower, more detailed approach if you try to explain backtracking to them.
+> Likewise if they're not familiar with lists.
+>
+> The same applies to presenting external resources.
+> If they only just started programming, pasting a link to the Python documentation is going to be of little help.
+> They are unlikely to be able to get around that website and understand what you expect them to read. In contrast, for a more seasoned programmer, the docs might be just what they need.
+
+
+## Add a Grain of Salt
+
+Giving people more information is generally a good thing, but it's important to remember that the person helped is trying to soak in as much as they can.
+Too much irrelevant information can confuse them, and make them lose sight of the actual solution.
+Presenting a solution that is considered a bad practice might be useful in certain situations, but it's important to make sure they are aware it's not a good solution, so they don't start using it themselves.
+
+> #### Example 4:
+>
+> **user**: "How can I print all elements in a list?"<br>
+> **helper1**: "You can do it like so:"<br>
+>
+> for element in your_list:
+> print(element)
+>
+> **helper2**: "You can also do it like this:"<br>
+>
+> for i in range(len(your_list)):
+> print(your_list[i])
+>
+> The second replier gave a valid solution, but it's important that he clarifies that it is concidered a bad practice in Python, and that the first solution should usually be used in this case.
+
+
+## It's OK to Step Away
+
+Sometimes you might discover you don't have the best chemistry with the person you're talking to.
+Maybe there's a language barrier you don't manage to overcome.
+In other cases, you might find yourself getting impatient or sarcastic, maybe because you already answered the question being asked three times in the past hour.
+
+That's OK- remember you can step away at any time and let others take over.
+You're here helping others on your own free time (and we really appreciate it!), and have no quotas to fill.
+
+At other times, you might start talking with someone and realize you're not experienced in the topic they need help with.
+There's nothing wrong with admitting you lack the specific knowledge required in this case, and wishing them good luck.
+We can't know everything.
+
+Remember that helping means thinking of what's best for them, and we also wouldn't want to see you become agitated.
+We're all here because we enjoy doing this.
+
+
+## Respect Others Giving Help
+
+You might sometimes see others giving help and guiding others to an answer.
+Throwing in additional ideas is great, but please remember both teaching and learning takes concentration, and you stepping in might break it.
+You might have another idea to suggest, but you see that there's already a person helping, and that they're handling the situation.
+In that case, it might be a good idea to patiently observe, and wait for a good opportunity to join in so as to not be disruptive.
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/how-to-contribute-a-page.md b/pydis_site/apps/content/resources/guides/pydis-guides/how-to-contribute-a-page.md
new file mode 100644
index 00000000..716250b1
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/how-to-contribute-a-page.md
@@ -0,0 +1,217 @@
+---
+title: How to Contribute a Page
+description: Learn how to write and publish a page to this website.
+icon: fas fa-info
+relevant_links:
+ Contributing to Site: https://pythondiscord.com/pages/contributing/site/
+ Using Git: https://pythondiscord.com/pages/contributing/working-with-git/
+toc: 4
+---
+
+Pages, which include guides, articles, and other static content, are stored in markdown files in the `site` repository on Github.
+If you are interested in writing or modifying pages seen here on the site, follow the steps below.
+
+For further assistance and help with contributing pages, send a message to the `#dev-contrib` channel in the Discord server!
+
+## Prerequisites
+Before working on a new page, you have to [setup the site project locally](https://pythondiscord.com/pages/contributing/site/).
+It is also a good idea to familiarize yourself with the [git workflow](https://pythondiscord.com/pages/contributing/working-with-git/), as it is part of the contribution workflow.
+
+Additionally, please submit your proposed page or modification to a page as an [issue in the site repository](https://github.com/python-discord/site/issues), or discuss it in the `#dev-contrib` channel in the server.
+As website changes require staff approval, discussing the page content beforehand helps with accelerating the contribution process, and avoids wasted work in the event the proposed page is not accepted.
+
+## Creating the Page
+All pages are located in the `site` repo, at the path `pydis_site/apps/content/resources/`. This is the root folder, which corresponds to the URL `www.pythondiscord.com/pages/`.
+
+For example, the file `pydis_site/apps/content/resources/hello-world.md` will result in a page available at `www.pythondiscord.com/pages/hello-world`.
+
+#### Page Categories
+Nested folders represent page categories on the website. Each folder under the root folder must include a `_info.yml` file with the following:
+
+```yml
+title: Category name
+description: Category description
+icon: fas fa-folder # Optional
+```
+
+All the markdown files in this folder will then be under this category.
+
+#### Having the Category Also Be a Page
+In order to make categories a page, just create a page **with the same name as the category folder** in the category's parent directory.
+
+```plaintext
+guides
+├── contributing.md
+├── contributing
+│   ├── _info.yml
+│   └── bot.md
+└── _info.yml
+```
+
+In the above example, `www.pythondiscord.com/guides/` will list `Contributing` as a category entry with information from `contributing/_info.yml`.
+
+However, `www.pythondiscord.com/guides/contributing` will render `contributing.md` rather than show the category contents.
+A dropdown menu will be automatically generated in the top right corner of the page listing the children of the category page.
+
+Therefore, `www.pythondiscord.com/guides/contributing/bot` will then render `bot.md`, with backlinks to `contributing.md`.
+
+## Writing the Page
+Files representing pages are in `.md` (Markdown) format, with all-lowercase filenames and spaces replaced with `-` characters.
+
+Each page must include required metadata, and optionally additional metadata to modify the appearance of the page.
+The metadata is written in YAML, and should be enclosed in triple dashes `---` *at the top of the markdown file*.
+
+**Example:**
+```yaml
+---
+title: How to Contribute a Page
+description: Learn how to write and publish a page to this website.
+icon: fas fa-info
+relevant_links:
+ Contributing to Site: https://pythondiscord.com/pages/contributing/site/
+ Using Git: https://pythondiscord.com/pages/contributing/working-with-git/
+---
+
+Pages, which include guides, articles, and other static content,...
+```
+
+### Required Fields
+- **title:** Easily-readable title for your article.
+- **description:** Short, 1-2 line description of the page's content.
+
+### Optional Fields
+- **icon:** Icon for the category entry for the page. Default: `fab fa-python` <i class="fab fa-python is-black" aria-hidden="true"></i>
+- **relevant_links:** A YAML dictionary containing `text:link` pairs. See the example above.
+- **toc:** A number representing the smallest heading tag to show in the table of contents.
+ See: [Table of Contents](#table-of-contents)
+
+## Extended Markdown
+
+Apart from standard Markdown, certain additions are available:
+
+### Abbreviations
+HTML `<abbr>` tags can be used in markdown using this format:
+
+**Markdown:**
+```nohighlight
+This website is HTML generated from YAML and Markdown.
+
+*[HTML]: Hyper Text Markup Language
+*[YAML]: YAML Ain't Markup Language
+```
+
+**Output:**
+
+This website is <abbr title="Hyper Text Markup Language">HTML</abbr>
+generated from <abbr title="YAML Ain't Markup Language">YAML</abbr> and Markdown.
+
+---
+
+### Footnotes
+**Markdown:**
+```nohighlight
+This footnote[^1] links to the bottom[^custom_label] of the page[^3].
+
+[^1]: Footnote labels start with a caret `^`.
+[^3]: The footnote link is numbered based on the order of the labels.
+[^custom label]: Footnote labels can contain any text within square brackets.
+```
+
+**Output:**
+
+This footnote[^1] links to the bottom[^custom label] of the page[^3].
+
+[^1]: Footnote labels start with a caret `^`.
+[^3]: The footnote link is numbered based on the order of the labels.
+[^custom label]: Footnote labels can contain any text within square brackets.
+
+---
+
+### Tables
+
+**Markdown:**
+```nohighlight
+| This is header | This is another header |
+| -------------- | ---------------------- |
+| An item | Another item |
+```
+
+**Output:**
+
+| This is header | This is another header |
+| -------------- | ---------------------- |
+| An item | Another item |
+
+---
+
+### Codeblock Syntax Highlighting
+Syntax highlighting is provided by `highlight.js`.
+To activate syntax highlighting, put the language directly after the starting backticks.
+
+**Markdown:**
+````nohighlight
+```python
+import os
+
+path = os.path.join("foo", "bar")
+```
+````
+
+**Output:**
+```python
+import os
+
+path = os.path.join("foo", "bar")
+```
+
+---
+
+### HTML Attributes
+To add HTML attributes to certain lines/paragraphs, [see this page](https://python-markdown.github.io/extensions/attr_list/#the-list) for the format and where to put it.
+
+This can be useful for setting the image size when adding an image using markdown (see the [Image Captions](#image-captions) section for an example), or for adding bulma styles to certain elements (like the warning notification [here](/pages/guides/pydis-guides/contributing/sir-lancebot#setup-instructions)).<br>
+**This should be used sparingly, as it reduces readability and simplicity of the article.**
+
+---
+
+### Image Captions
+To add an image caption, place a sentence with italics *right below* the image link
+
+**Markdown:**
+```nohighlight
+![Summer Code Jam 2020](/static/images/events/summer_code_jam_2020.png){: width="400" }
+*Summmer Code Jam 2020 banner with event information.*
+```
+
+**Output:**
+
+![Summer Code Jam 2020](/static/images/events/summer_code_jam_2020.png){: width="400"}
+*Summer Code Jam 2020 banner with event information.*
+
+> Note: To display a regular italicized line below an image, leave an empty line between the two.
+
+---
+
+### Table of Contents
+In order to show the table of contents on a page, simply define the `toc` key in the page metadata.
+
+The value of the `toc` key corresponds to the smallest heading to list in the table of contents.
+For example, with markdown content like this:
+
+```markdown
+# Header 1
+words
+### Header 3
+more words
+# Another Header 1
+## Header 2
+even more words
+```
+
+and `toc: 2` in the page metadata, only `Header 1`, `Another Header 1` and `Header 2` will be listed in the table of contents.
+
+To use a custom label in the table of contents for a heading, set the `data-toc-label` attribute in the heading line. See [HTML Attributes](#html-attributes) for more information.
+
+```markdown
+# Header 1 {: data-toc-label="Header One" }
+```
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/off-topic-etiquette.md b/pydis_site/apps/content/resources/guides/pydis-guides/off-topic-etiquette.md
new file mode 100644
index 00000000..f8031834
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/off-topic-etiquette.md
@@ -0,0 +1,32 @@
+---
+title: Conversation Etiquette in Our Off-Topic Channels
+description: Guidelines on conversation etiquette.
+icon: fab fa-discord
+---
+
+## Why do we need off-topic etiquette?
+Everyone wants to have good conversations in our off-topic channels, but with tens of thousands of members, this might mean different things to different people.
+To facilitate the best experience for everyone, here are some guidelines on conversation etiquette.
+
+## Three things you shouldn't do
+1. Don't interrupt active conversations
+ * There's three off-topic channels which can support three simultaneous conversations.
+ If one is active and you have something you'd like to discuss, try a different channel.
+2. Don't post memes unless they're relevant to a conversation
+ * There are better places to share memes; if you have a meme you think is worth sharing, try to find a relevant subreddit, like [/r/ProgrammerHumor](https://www.reddit.com/r/ProgrammerHumor/).
+3. Don't snap at people
+ * We are a large, diverse community. Different native languages, experiences, and ages mean miscommunications happen. Always try to assume the best in other community members.
+
+## Three things you should do
+1. Ask away
+ * If you have a question that isn't about Python, just ask it in an inactive off-topic channel.
+ If someone sees your question who knows the answer, they will answer you.
+ "Why is my wifi not working?", "how do I tune a guitar?", "is there a server for C#?", are all fair game for questions to ask.
+ If your question relates to Python, try to find the most suitable channel to ask your question, or open a help session.
+2. When in doubt, ask someone to clarify what they mean
+ * If you're not sure you properly understand someone, ask them to clarify.
+ Text isn't necessarily the easiest way for everyone to communicate, so it makes life easier if we're all on the same page.
+3. Join in!
+ * The off-topic channels have lots of fun or interesting conversations; if someone is talking about something you're interested in, don't be scared to hop into the conversation.
+
+While you can discuss other topics than Python in the off-topic channels, the [ordinary rules](/pages/rules/) still apply.
diff --git a/pydis_site/apps/content/resources/guides/python-guides/_info.yml b/pydis_site/apps/content/resources/guides/python-guides/_info.yml
new file mode 100644
index 00000000..67730962
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/python-guides/_info.yml
@@ -0,0 +1,3 @@
+title: Python Guides
+description: Guides related to the Python Programming Language.
+icon: fab fa-python
diff --git a/pydis_site/apps/content/resources/guides/python-guides/creating-python-environment-windows.md b/pydis_site/apps/content/resources/guides/python-guides/creating-python-environment-windows.md
new file mode 100644
index 00000000..356d63bd
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/python-guides/creating-python-environment-windows.md
@@ -0,0 +1,72 @@
+---
+title: Creating a Unix-style Python Environment on Windows
+description: How to setup Python for Windows.
+---
+
+Many programmers use Linux or macOS operating systems for their work, though newcomers to programming will likely want to get started on the computer they already own, which will often be running Windows.
+This guide will help you install Python on Windows.
+
+Programmers also need to become comfortable using a command prompt (also known as a terminal), and many guides for both beginning and advanced programming will often tell you certain commands to run.
+The Windows command prompt has different names for similar commands that are available on Linux and macOS.
+This guide will also help you set up a command prompt called Git Bash, which will support many of the commands available on Linux and macOS.
+
+## Installing Python
+Python can be downloaded from the Python website on the [downloads page](https://www.python.org/downloads/).
+The website will automatically present you with a download button for the latest release of the Windows version when you access the site from a Windows machine.
+
+Once the download is complete, you can begin the installation.
+Select "Customize Installation".
+The default settings for "Optional Features" are sufficient and you can click "Next".
+
+The next step is to decide on a location where the Python executable can be stored on your computer.
+This should be a location that's easy for you to remember.
+One possibility is to create a folder called "Python" at the root of your hard drive.
+Once you have selected a location, you can click "Install", as no other settings on this screen need to be adjusted.
+This will complete the installation.
+
+## Installing a text editor
+You will also need a text editor for writing Python programs, and for subsequent steps of this guide.
+Powerful programs called integrated development environments (IDEs) like PyCharm and Visual Studio Code contain text editors, but they also contain many other features with uses that aren't immediately obvious to new programmers.
+
+[Notepad++](https://notepad-plus-plus.org/) is a popular text editor for both beginners and advanced users who prefer a simpler interface.
+Other editors we recommend can be found (https://pythondiscord.com/resources/tools/#editors)[here].
+
+## Installing Git Bash
+Git is a command line program that helps you keep track of changes to your code, among other things.
+Many developers use it, and while you may not need it right away, it is useful to install it because it comes with Git Bash.
+On the "Select Components" screen, no settings need to be changed.
+The next screen will ask what text editor you want to use with Git. Vim is the default choice, though Vim is widely considered difficult to learn, so you may choose to select Notepad++ or whichever text editor you may have installed previously.
+
+For all remaining screens in the installation, the default selections are fine.
+
+## Configuring .bashrc
+`.bashrc` is a file where we tell Git Bash where the Python executable is.
+First, open Git Bash, and as your first command, type `echo ~` and hit enter.
+This will most likely print `c/Users/YourUsername` to the terminal.
+Navigate to this location in your file explorer, though keep in mind that Windows will display `c/Users/YourUsername` as `C:\Users\YourUsername`.
+In this folder, there will be a file called `.bashrc`; open it with your text editor of choice.
+
+For this step, you will need to remember where you installed Python earlier.
+In whichever folder that was, there is a file called `python.exe`; this is the executable that will run your Python programs.
+Copy the full path of this file, starting from `C:`.
+If you used the example location given earlier, it will be located at `C:\Python\python.exe`.
+
+In the `.bashrc` file, add a line to the end of the file saying `alias python='C:\\Python\\python.exe`, where `C:\\Python\\python.exe` is the location of your `python.exe` file, but each folder is separated by two backslashes instead of one.
+The two backslashes are because a single backslash is used as an [escape character](https://en.wikipedia.org/wiki/Escape_character).
+Save the file, and then type `source ~/.bashrc` to activate the change you have made.
+
+Finally, enter `python -c 'import sys; print(sys.executable)'` into Git Bash.
+(If you attempt to copy and paste this into the terminal using Ctrl+V, it might not work, though Shift+Insert will.)
+If all the steps have been followed correctly, this will print the location of your `python.exe` file and demonstrate that your environment is set up correctly.
+You can hereafter use the `python` command in Git Bash to run any Python program that you write.
+
+## Running a test program
+At any location on your computer, create a file named `hello.py` and open it with your text editor.
+The program need only be one line: `print('Hello world!')`.
+Save this file.
+
+To run this program in Git Bash, navigate to where it is saved on your hard drive.
+If you know the path to this location, you can use the `cd` command ("cd" stands for "change directory") to navigate to it.
+If it's saved to your desktop, `cd /c/Users/YourUsername/Desktop` will take you there.
+Otherwise if you have the directory open in your file explorer, you can right click anywhere in the white space of the file explorer window (not on top of a file) and select "Git Bash Here".
+Once you're there, type `python hello.py`, and the program will run.
diff --git a/pydis_site/apps/content/resources/guides/python-guides/discordpy.md b/pydis_site/apps/content/resources/guides/python-guides/discordpy.md
new file mode 100644
index 00000000..b0b2fad1
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/python-guides/discordpy.md
@@ -0,0 +1,230 @@
+---
+title: Discord.py Learning Guide
+description: A learning guide for the discord.py bot framework written by members of our community.
+icon: fab fa-python
+toc: 2
+---
+
+<!-- discord.py Badge -->
+<a href="https://github.com/Rapptz/discord.py/">
+ <div class="tags has-addons">
+ <span class="tag is-dark">discord.py</span><span class="tag is-info">≥1.0</span>
+ </div>
+</a>
+
+Interest in creating a Discord bot is a common introduction to the world of programming in our community.
+
+Using it as your first project in programming while trying to learn is a double-edged sword.
+A large number of concepts need to be understood before becoming proficient at creating a bot, making the journey of learning and completing the project more arduous than more simple projects designed specifically for beginners.
+However in return, you get the opportunity to expose yourself to many more aspects of Python than you normally would and so it can be an amazingly rewarding experience when you finally reach your goal.
+
+Another excellent aspect of building bots is that it has a huge scope as to what you can do with it, almost only limited by your own imagination.
+This means you can continue to learn and apply more advanced concepts as you grow as a programmer while still building bots, so learning it can be a useful and enjoyable skillset.
+
+This page provides resources to make the path to learning as clear and easy as possible, and collates useful examples provided by the community that may address common ideas and concerns that are seen when working on Discord bots.
+
+## Essential References
+
+Official Documentation: [https://discord.py.readthedocs.io](https://discordpy.readthedocs.io/)
+
+Source Repository: [https://github.com/Rapptz/discord.py](https://github.com/Rapptz/discord.py)
+
+## Creating a Discord Bot Account
+
+1. Navigate to [https://discord.com/developers/applications](https://discord.com/developers/applications) and log in.
+2. Click on `New Application`.
+3. Enter the application's name.
+4. Click on `Bot` on the left side settings menu.
+5. Click `Add Bot` and confirm with `Yes, do it!`.
+
+### Client ID
+Your Client ID is the same as the User ID of your Bot.
+You will need this when creating an invite URL.
+
+You can find your Client ID located on the `General Information` settings page of your Application, under the `Name` field.
+
+Your Client ID is not a secret, and does not need to be kept private.
+
+### Bot Token
+
+Your Bot Token is the token that authorises your Bot account with the API.
+Think of it like your Bot's API access key.
+With your token, you can interact with any part of the API that's available to bots.
+
+You can find your Bot Token located on the Bot settings page of your Application, under the Username field.
+You can click the Copy button to copy it without revealing it manually.
+
+**Your Bot Token is a secret, and must be kept private.**
+If you leak your token anywhere other people has access to see it, no matter the duration, you should reset your Bot Token.
+
+To reset your token, go to the Bot settings page of your Application, and click the Regenerate button.
+Be sure to update the token you're using for your bot script to this new one, as the old one will not work anymore.
+
+### Permissions Integer
+
+Discord Permissions are typically represented by a Permissions Integer which represents all the Permissions that have been allowed.
+
+You can find a reference to all the available Discord Permissions, their bitwise values and their descriptions here:<br>
+[https://discordapp.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags](https://discordapp.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags)
+
+If you want to create your own Permissions Integer, you can generate it in the `Bot` settings page of your Application, located at the bottom of the page.
+
+Tick the permissions you want to be allowing, and it'll update the `Permissions Integer` field, which you can use in your Bot Invite URL to set your bot's default permissions when users go to invite it.
+
+### Bot Invite URL
+
+Bot's cannot use a server invite link. Instead, they have to be invited by a member with the Manage Server permission.
+
+The Bot Invite URL is formatted like:
+`https://discordapp.com/oauth2/authorize?client_id={CLIENT_ID}&scope=bot&permissions={PERMISSIONS_INTEGER}`
+
+You can create the Invite URL for your bot by replacing:
+
+* `{CLIENT_ID}` with your [Client ID](#client-id)
+* `{PERMISSIONS_INTEGER}` with the [Permissions Integer](#permissions-integer)
+
+You can also generate it with the [Permissions Calculator](https://discordapi.com/permissions.html tool) tool.
+
+## Using the Basic Client (`discord.Client`) { data-toc-label="Using the Basic Client" }
+
+Below are the essential resources to read over to get familiar with the basic functionality of `discord.py`.
+
+* [Basic event usage](https://discordpy.readthedocs.io/en/latest/intro.html#basic*concepts)
+* [Simple bot walkthrough](https://discordpy.readthedocs.io/en/latest/quickstart.html#a*minimal*bot)
+* [Available events reference](https://discordpy.readthedocs.io/en/latest/api.html#event*reference)
+* [General API reference](https://discordpy.readthedocs.io/en/latest/api.html)
+
+## Using the Commands Extension (`commands.Bot`) { data-toc-label="Using the Commands Extension" }
+
+The Commands Extension has a explanatory documentation walking you through not only what it is and it's basic usage, but also more advanced concepts.
+Be sure to read the prose documentation in full at:<br>
+[https://discordpy.readthedocs.io/en/latest/ext/commands/commands.html](https://discordpy.readthedocs.io/en/latest/ext/commands/commands.html)
+
+It fully covers:
+* How to create bot using the Commands Extension
+* How to define commands and their arguments
+* What the Context object is
+* Argument Converters
+* Error Handling basics
+* Command checks
+
+You will also need to reference the following resources:
+* [Commands Extension exclusive events](https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#event-reference)
+* [Commands Extension API reference](https://discordpy.readthedocs.io/en/latest/ext/commands/api.html)
+
+## FAQ
+
+The documentation covers some basic FAQ's, and they are recommended to be read beforehand, and referenced before asking for help in case it covers your issue:
+[https://discordpy.readthedocs.io/en/latest/faq.html](https://discordpy.readthedocs.io/en/latest/faq.html)
+
+## Usage Examples
+
+### Official Examples and Resources
+
+The official examples can be found on the [source repository](https://github.com/Rapptz/discord.py/tree/master/examples).
+
+The most commonly referenced examples are:
+
+* [Basic Commands Extension Bot](https://github.com/Rapptz/discord.py/blob/master/examples/basic_bot.py)
+* [Background Task Example](https://github.com/Rapptz/discord.py/blob/master/examples/background_task.py)
+
+### Permissions Documentation
+
+* [Role Management 101](https://support.discordapp.com/hc/en-us/articles/214836687-Role-Management-101)
+* [Full Permissions Documentation](https://discordapp.com/developers/docs/topics/permissions)
+
+### Community Examples and Resources
+
+The `discord.py` developer community over time have shared examples and references with each other.<br>
+The following are a collated list of the most referenced community examples.
+
+#### Extensions / Cogs
+* [Extension/Cog Example](https://gist.github.com/EvieePy/d78c061a4798ae81be9825468fe146be) - *Credit to EvieePy*
+* [Available Cog Methods](https://gist.github.com/Ikusaba-san/69115b79d33e05ed07ec4a4f14db83b1) - *Credit to MIkusaba*
+
+#### Error Handling
+* [Decent Error Handling Example](https://gist.github.com/EvieePy/7822af90858ef65012ea500bcecf1612) - *Credit to EvieePy*
+
+#### Embeds
+* [Embed Live Designer and Visualiser](https://leovoel.github.io/embed-visualizer/) - *Credit to leovoel*
+* [Embed Element Reference](https://cdn.discordapp.com/attachments/84319995256905728/252292324967710721/embed.png)<br>
+![Embed Element Reference](/static/images/content/discordpy_embed.png){: width="200" }
+
+##### Using Local Images in Embeds
+```py
+filename = "image.png"
+
+f = discord.File("some_file_path", filename=filename)
+embed = discord.Embed()
+
+embed.set_image(url=f"attachment://{filename}")
+await messagable.send(file=f, embed=embed)
+```
+
+##### Embed Limits
+
+| **Element** | **Characters** |
+| -------------- | ---------------------- |
+| Title | 256 |
+| Field Name | 256 |
+| Field Value | 1024 |
+| Description | 2048 |
+| Footer | 2048 |
+| **Entire Embed** | **6000**
+
+| **Element** | **Count** |
+| -------------- | ---------------------- |
+| Fields | 25 |
+
+#### Emoji
+
+- [Bot's Using Emoji](https://gist.github.com/scragly/b8d20aece2d058c8c601b44a689a47a0)
+
+#### Activity Presence
+
+- [Setting Bot's Discord Activity](https://gist.github.com/scragly/2579b4d335f87e83fbacb7dfd3d32828)
+
+#### Image Processing
+
+- [PIL Image Processing Example Cog](https://gist.github.com/Gorialis/e89482310d74a90a946b44cf34009e88) - *Credit to Gorialis*
+
+### Systemd Service
+**botname.service**<br>
+```ini
+[Unit]
+Description=My Bot Name
+After=network-online.target
+
+[Service]
+Type=simple
+WorkingDirectory=/your/bots/directory
+ExecStart=/usr/bin/python3 /your/bots/directory/file.py
+User=username
+Restart=on-failure
+
+[Install]
+WantedBy=network-online.target
+```
+
+**Directory**<br>
+`/usr/local/lib/systemd/system`
+
+**Service Commands**<br>
+Refresh systemd after unit file changes:<br>
+`systemctl daemon-reload`
+
+Set service to start on boot:<br>
+`systemctl enable botname`
+
+Start service now:<br>
+`systemctl start botname`
+
+Stop service:<br>
+`systemctl stop botname`
+
+**Viewing Logs**<br>
+All logs:<br>
+`journalctl -u botname`
+
+Recent logs and continue printing new logs live:<br>
+`journalctl -fu mybot`
diff --git a/pydis_site/apps/content/resources/guides/python-guides/mutability.md b/pydis_site/apps/content/resources/guides/python-guides/mutability.md
new file mode 100644
index 00000000..185dc87c
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/python-guides/mutability.md
@@ -0,0 +1,55 @@
+---
+title: Mutability and Immutability in Python
+description: "Mutable and immutable data types: what they are and how they work."
+---
+
+Consider this example:
+```python
+>>> s = "hello"
+>>> s.upper()
+'HELLO'
+>>> s
+'hello'
+```
+This might break your expectations.
+After all, you've called the `upper()` method on `s`, so why didn't it change? That's because strings are _immutable_: you can't change them in-place, only create new ones.
+In this example, `.upper()` just cannot change the string stored in `s`.
+
+How do you make `s` store `'HELLO'` instead of `'hello'` then? That's possible.
+Even though you can't change the original string, you can create a new one, which is like the old one, but with all letters in upper case.
+
+In other words, `s.upper()` doesn't change an existing string.
+It just returns a new one.
+```python
+>>> s = 'hello'
+>>> s = s.upper()
+>>> s
+'HELLO'
+```
+
+Let's examine what's going on here.
+At first, the variable `s` refers to some object, the string `'hello'`.
+
+![s refers to the string "hello"](/static/images/content/mutability/s_refers_hello.png)
+
+When you call `s.upper()`, a new string, which contains the characters `'HELLO'`, gets created.
+
+![s.upper creates "HELLO"](/static/images/content/mutability/s_upper_creates_HELLO.png)
+
+This happens even if you just call `s.upper()` without any assignment, on its own line:
+```python
+"hello".upper()
+```
+In this case, a new object will be created and discarded right away.
+
+Then the assignment part comes in: the name `s` gets disconnected from `'hello'`, and gets connected to `'HELLO'`.
+
+![s gets assigned to "HELLO"](/static/images/content/mutability/s_gets_assigned_to_HELLO.png)
+
+Now we can say that `'HELLO'` is stored in the `s` variable.
+
+Then, because no variables refer to the _object_ `'hello'`, it gets eaten by the garbage collector.
+
+!["hello" Gets Eaten](/static/images/content/mutability/hello_gets_eaten.png)
+
+It means that the memory reserved for that object will be freed. If that didn't happen, the 'garbage' would accumulate over time and fill up all the RAM.
diff --git a/pydis_site/apps/content/resources/guides/python-guides/parameters-and-arguments.md b/pydis_site/apps/content/resources/guides/python-guides/parameters-and-arguments.md
new file mode 100644
index 00000000..45ad60b1
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/python-guides/parameters-and-arguments.md
@@ -0,0 +1,290 @@
+---
+title: Function Parameters and Arguments in Python
+description: An in-depth look at function parameters and arguments, and how to use them.
+toc: 1
+---
+
+A Python function is utilised in two steps:
+
+1. The function definition/signature (used just once).
+2. The function invocation/call (used many times).
+
+The function definition uses parameters, whereas the function call uses arguments:
+
+```python
+def foo(this_is_a_parameter):
+ print(this_is_a_parameter)
+
+foo(this_is_an_argument)
+```
+
+An important detail to be aware of is that by default any argument used to call a function in Python can be used as both a positional and a keyword argument—just not at the same time.
+A function call may contain a mixture of positional and keyword arguments, and—unless otherwise specified—an argument can reference the parameters in the function definition positionally, or by name (keyword).
+
+# Positional Arguments
+
+```python
+def foo(a, b, c):
+ print(a, b, c)
+
+>>> foo(1, 2, 3)
+1 2 3
+```
+
+In the above function definition we have three parameters `a`, `b`, and `c`.
+
+When we invoke the function with the arguments `1`, `2`, and `3`, the function will map these values in the exact order given to the parameters in the function definition.
+With no keyword reference given they become positional arguments.
+
+# Keyword Arguments
+
+```python
+def foo(a, b, c):
+ print(a, b, c)
+
+>>> foo(1, 2, 3)
+1 2 3
+
+>>> foo(c=3, b=2, a=1)
+1 2 3
+```
+
+As you can see, `foo(1, 2, 3)` and `foo(c=3, b=2, a=1)` are identical.
+Referencing a function parameter by its name means that we are using a keyword argument.
+The order in which keyword arguments are given does not matter.
+
+# Mixing Positional and Keyword Arguments
+
+So what happens if we want to mix the positional argument mapping with keyword arguments?
+
+Python prioritises the mapping of positional arguments to their parameter names before the mapping of keywords.
+
+```python
+def foo(a, b, c):
+ print(a, b, c)
+
+>>> foo(1, c=3, b=2)
+1 2 3
+```
+
+Passing a keyword argument using the name of a parameter that has already been given will not work:
+
+```python
+>>> foo(1, 2, a=3)
+TypeError: foo() got multiple values for argument 'a'
+
+>>> foo(1, b=2, b=3)
+SyntaxError: keyword argument repeated
+```
+
+Attempting to pass positional arguments after a keyword argument will also not work:
+
+```python
+>>> foo(a=1, 2, 3)
+SyntaxError: positional argument follows keyword argument
+```
+
+# Default Parameter Values
+
+Although the syntax is similar, these are not to be confused with keyword arguments.<br>
+Default parameter values appear within the function definition and allow us to conveniently set a default value. This means that if any argument is omitted, its default value will be used as the argument.
+
+```python
+def foo(a=0, b=0, c=0):
+ print(a, b, c)
+
+>>> foo()
+0 0 0
+
+>>> foo(1, 2, 3)
+1 2 3
+
+>>> foo(c=3, b=2)
+0 2 3
+
+>>> foo(1, c=3)
+1 0 3
+```
+
+Using default parameter values does not change how a function can be invoked with arguments:
+
+```python
+>>> foo(1, 2, a=3)
+TypeError: foo() got multiple values for argument 'a'
+
+>>> foo(1, b=2, b=3)
+SyntaxError: keyword argument repeated
+
+>>> foo(a=1, 2, 3)
+SyntaxError: positional argument follows keyword argument
+```
+
+You must specify any parameters without a default value before those with default values:
+
+```python
+def foo(a=0, b):
+ ^
+SyntaxError: non-default argument follows default argument
+```
+
+# Positional-only Parameters
+[Python 3.8](https://docs.python.org/3/whatsnew/3.8.html#positional-only-parameters) / [PEP 570](https://www.python.org/dev/peps/pep-0570/) introduces the possibility to specify which parameters are required to be positional-only via a bare `/` parameter within a function definition.
+
+```python
+def foo(a=0, b=0, /, c=0, d=0):
+ print(a, b, c, d)
+```
+
+The parameters defined before the bare `/` are now considered to be positional-only and keyword mapping will no longer work on them.<br>
+In the above function definition `a` and `b` are now positional-only parameters.
+
+These function calls will still work:
+
+```python
+>>> foo()
+0 0 0 0
+
+>>> foo(1)
+1 0 0 0
+
+>>> foo(1, 2, 3, 4)
+1 2 3 4
+
+>>> foo(1, 2, d=4, c=3)
+1 2 3 4
+
+>>> foo(1, d=4, c=3)
+1 0 3 4
+
+>>> foo(c=3, d=4)
+0 0 3 4
+```
+
+However, attempting to pass keyword arguments for `a` or `b` will fail:
+
+```python
+>>> foo(1, b=2, c=3, d=4)
+TypeError: foo() got some positional-only arguments passed as keyword arguments: 'b'
+```
+
+### Q: Why is this useful?
+
+#### Keyword Argument Freedom
+
+Passing a keyword argument using the name of a parameter that has already been given will not work.
+This becomes an issue if we require keyword arguments that use the same parameter names as defined in the function signature, such as via callback functions.
+
+```python
+def foo(a, **kwargs):
+ print(a, kwargs)
+
+>>> foo(a=1, a=2)
+SyntaxError: keyword argument repeated
+
+>>> foo(1, a=2)
+TypeError: foo() got multiple values for argument 'a'
+```
+
+#### Backwards Compatibility
+
+Because Python allows that an argument by default can be either positional or keyword, a user is free to choose either option.
+Unfortunately, this forces the author to keep the given parameter names as they are if they wish to support backwards compatibility, as changing the parameter names can cause dependent code to break.
+Enforcing positional-only parameters gives the author the freedom to separate the variable name used within the function from its usage outside of it.
+
+```python
+def calculate(a, b):
+ # do something with a and b
+
+>>> calculate(1, 2)
+```
+
+A user could call this function using `a` or `b` as keywords, which the author may have not intended:
+
+```python
+>>> calculate(a=1, b=2)
+```
+
+However, by using `/`, the user will no longer be able to invoke using `a` or `b` as keywords, and the author is also free to rename these parameters:
+
+```python
+def calculate(x, y, /):
+ # do something with x and y
+
+>>> calculate(1, 2)
+```
+
+# Keyword-only Parameters
+
+Similarly to enforcing positional-only parameters, we can also enforce keyword-only parameters using a bare `*` parameter.
+The parameters defined after the bare `*` are now considered to be keyword-only.
+
+```python
+def foo(a=0, b=0, /, c=0, *, d=0):
+ print(a, b, c, d)
+
+>>> foo()
+0 0 0 0
+
+>>> foo(1, 2, 3)
+1 2 3 0
+
+>>> foo(1, 2, d=4, c=3)
+1 2 3 4
+
+>>> foo(1, d=4, c=3)
+1 0 3 4
+```
+
+Although `c` can be either a positional or keyword argument, if we attempt to pass `d` as a non-keyword argument, it will fail:
+
+```python
+>>> foo(1, 2, 3, 4)
+TypeError: foo() takes from 0 to 3 positional arguments but 4 were given
+```
+
+At least one named parameter must be provided after a bare `*` parameter.
+Writing a function definition similar to what is shown below would not make sense, as without the context of a named parameter the bare `*` can simply be omitted.
+
+```python
+def foo(a=0, *, **kwargs):
+ ^
+SyntaxError: named arguments must follow bare *
+```
+
+### Q: Why is this useful?
+
+The main benefit of using keyword-only parameters is when they are used together with positional-only parameters to remove ambiguity.
+
+However, it may sometimes also be desirable to use keyword-only arguments on their own.<br>
+If we were to expose a function as part of an API, we may want the parameter names to carry explicit meaning.
+
+Without using keyword names when invoking the function it can be unclear as to what the provided arguments are for.
+Additionally, a user could also choose to interchange positional arguments with keyword arguments, which can potentially add to the confusion.
+
+```python
+def update(identity=None, name=None, description=None):
+ # handle the parameters
+
+>>> update("value 1", "value 2", "value 3")
+
+>>> update(1234, "value 1", description="value 2")
+```
+
+Enforcing the keyword names is clearer, as it carries context without needing to look at the function definition:
+
+```python
+def update(*, identity=None, name=None, description=None):
+ # handle the parameters
+
+>>> update(identity=1234, name="value 1", description="value 2")
+```
+
+# Summary
+
+* Unless otherwise specified, an argument can be both positional and keyword.
+* Positional arguments, when provided, must be in sequence.
+* Positional arguments must be used before keyword arguments.
+* Keyword arguments may be in any order.
+* A default parameter value is used when the argument is omitted.
+* A bare `/` used as a parameter in a function definition enforces positional-only parameters to its left.
+* A bare `*` used as a parameter in a function definition enforces keyword-only parameters to its right.
diff --git a/pydis_site/apps/content/resources/privacy.md b/pydis_site/apps/content/resources/privacy.md
new file mode 100644
index 00000000..a2ab6f87
--- /dev/null
+++ b/pydis_site/apps/content/resources/privacy.md
@@ -0,0 +1,12 @@
+---
+title: Privacy Policy
+description: Our server's privacy policy.
+icon: fab fa-discord
+---
+
+You should be redirected. If you are not, [please click here](https://www.notion.so/pythondiscord/Python-Discord-Privacy-ee2581fea4854ddcb1ebc06c1dbb9fbd).
+
+<script>
+ // Redirect visitor to the privacy page
+ window.location.href = "https://www.notion.so/pythondiscord/Python-Discord-Privacy-ee2581fea4854ddcb1ebc06c1dbb9fbd";
+</script>
diff --git a/pydis_site/apps/content/resources/rules.md b/pydis_site/apps/content/resources/rules.md
new file mode 100644
index 00000000..27f03f07
--- /dev/null
+++ b/pydis_site/apps/content/resources/rules.md
@@ -0,0 +1,44 @@
+---
+title: Python Discord Rules
+description: The rules of our community.
+icon: fab fa-discord
+---
+We have a small but strict set of rules on our server. Please read over them and take them on board. If you don't understand a rule or need to report an incident, please send a direct message to <code>@ModMail</code>!
+
+> 1. Follow the [Discord Community Guidelines](https://discordapp.com/guidelines) and [Terms Of Service](https://discordapp.com/terms).
+> 2. Follow the [Python Discord Code of Conduct](/pages/code-of-conduct/).
+> 3. Listen to and respect staff members and their instructions.
+> 4. This is an English-speaking server, so please speak English to the best of your ability.
+> 5. Do not provide or request help on projects that may break laws, breach terms of services, be considered malicious or inappropriate. Do not help with ongoing exams. Do not provide or request solutions for graded assignments, although general guidance is okay.
+> 6. No spamming or unapproved advertising, including requests for paid work. Open-source projects can be shared with others in #python-general and code reviews can be asked for in a help channel.
+> 7. Keep discussions relevant to channel topics and guidelines.
+
+# Nickname Policy
+
+In order to keep things pleasant and workable for both users and staff members, we enforce the following requirements regarding your nickname.
+
+1. No blank or "invisible" names
+2. No slurs or other offensive sentiments
+3. No noisy unicode characters - for example, z̯̯͡a̧͎̺̻̝͕̠l̡͓̫̣g̹̲o̡̼̘ or byte order marks
+4. No nicknames designed to annoy other users
+
+Staff reserves the right to change the nickname of any user for any reason. Failure to comply with these requirements may result in you losing the right to change your nickname. We also reserve the right to discipline users with offensive usernames, regardless of the nickname they're using.
+
+
+# Infractions
+
+We have a generally no-nonsense policy when it comes to our rules. If you notice someone breaking them, feel free to mention or DM a staff member and we'll try to deal with it as soon as possible.
+
+The possible actions we take based on infractions can include the following:
+
+* A public verbal or textual warning
+* Forced nick changes, where appropriate
+* A short temporary mute
+* A long temporary mute
+* A kick from the server
+* A temporary ban from the server
+* A permanent ban from the server
+
+While we do discuss more serious matters internally before handing out a punishment, simpler infractions are dealt with directly by individual staffers and the punishment they hand out is left to their own discretion.
+
+If you receive an infraction and would like to appeal it, send an e-mail to [[email protected]](mailto:[email protected]).
diff --git a/pydis_site/apps/content/resources/security-notice.md b/pydis_site/apps/content/resources/security-notice.md
new file mode 100644
index 00000000..e3630ae1
--- /dev/null
+++ b/pydis_site/apps/content/resources/security-notice.md
@@ -0,0 +1,37 @@
+---
+title: Security Notice
+description: How vulnerabilities in our projects should be reported.
+icon: fas fa-shield-alt
+---
+
+This is the security notice for all Python Discord repositories.
+The notice explains how vulnerabilities should be reported.
+
+# Reporting a Vulnerability
+
+If you've found a vulnerability, we would like to know so we can fix it before it is released publicly.
+**Do not open a GitHub issue for a found vulnerability**.
+
+Send details to [[email protected]](mailto:[email protected]) or through a Discord direct message to an Admin of Python Discord, including:
+
+* the website, page or repository where the vulnerability can be observed
+* a brief description of the vulnerability
+* optionally the type of vulnerability and any related [OWASP category](https://www.owasp.org/index.php/Category:OWASP_Top_Ten_2017_Project)
+* non-destructive exploitation details
+
+We will do our best to reply as fast as possible.
+
+# Scope
+
+The following vulnerabilities **are not** in scope:
+
+* volumetric vulnerabilities, for example overwhelming a service with a high volume of requests
+* reports indicating that our services do not fully align with “best practice”, for example missing security headers
+
+If you aren't sure, you can still reach out via email or direct message.
+
+---
+
+This notice is inspired by the [GDS Security Notice](https://github.com/alphagov/.github/blob/master/SECURITY.md).
+
+*Version 2021-03*
diff --git a/pydis_site/apps/content/resources/server-info/_info.yml b/pydis_site/apps/content/resources/server-info/_info.yml
new file mode 100644
index 00000000..52df0f8d
--- /dev/null
+++ b/pydis_site/apps/content/resources/server-info/_info.yml
@@ -0,0 +1,3 @@
+title: Server Information
+description: Information on roles, tooling, and infrastructure at Python Discord.
+icon: fab fa-discord
diff --git a/pydis_site/apps/content/resources/server-info/roles.md b/pydis_site/apps/content/resources/server-info/roles.md
new file mode 100644
index 00000000..716f5b1e
--- /dev/null
+++ b/pydis_site/apps/content/resources/server-info/roles.md
@@ -0,0 +1,131 @@
+---
+title: Python Discord Server Roles
+description: Information on the various roles at Python Discord.
+icon: fab fa-discord
+---
+
+# Basic Roles
+
+### <span class="fas fa-circle" style="color:#6e6e6e"></span> Announcements
+**Description:** A role pinged by Admins when an announcement is made in the `#announcements` channel.
+
+**How to get it:** Run the command `!subscribe` in the `#bot-commands` channel.
+To unsubscribe, run `!unsubscribe` in the `#bot-commands` channel.
+
+
+### <span class="fas fa-circle" style="color:#6e6e6e"></span> Voice Verified
+**Description:** A role that lets people speak in voice channels.
+
+**How to get it:** Send `!voiceverify` in the `#voice-verification` channel.
+There are multiple requirements listed there for getting the role.
+
+---
+
+# Server Support Roles
+
+### <span class="fas fa-circle" style="color:#55cc6c"></span> Contributors
+**Description:** A role given by staff to people who make substantial contributions to any of the server's [open source repositories](https://github.com/python-discord/) (Sir Lancebot, Python, the site, the branding repo, etc..).<br>
+This includes writing pull requests for open issues, and also for reviewing open pull requests (**we really need reviewers!**)
+
+**How to get it:** Contribute to the projects!
+There is no minimum requirements, but the role is **not** assigned for every single contribution.
+Read more about this in the [Guidelines for the Contributors Role](/pages/contributing/#guidelines-for-the-contributors-role) on the Contributing page.
+
+---
+
+# Financial Support Roles
+
+### <span class="fas fa-circle" style="color:#46e6e8"></span> Nitro Boosters
+**Description:** A vanity role for people who boost the server with their nitro subscription.
+
+**How to get it:** Boost the server with a nitro subscription.
+
+
+### <span class="fas fa-circle" style="color:#46e6e8"></span> <span class="fas fa-circle" style="color:#3e7be9"></span> <span class="fas fa-circle" style="color:#2a82bd"></span> Patrons
+**Description:** A vanity role for Patreon patrons of the server.
+
+**How to get it:** [Become a patron here!](https://www.patreon.com/python_discord)
+
+---
+
+# Staff Roles
+#### Note regarding staff roles:
+##### Q: How do I apply for Helper/Moderator/Admin?
+There is no application, and there are no public nominations. Staff keep an eye out for potential candidates, and people nominated (by staff) are put in a pool for evaluation. After a period of time the candidate for a certain role is voted on by staff higher up the hierarchy.
+
+##### Q: How do I become Helper?
+See the description of a Helper. Being active in helping others, providing good help, contributing to our projects, and abiding by our rules go a long way towards catching staff attention, and make the server a better place for both beginners and advanced Python devs.
+
+##### Role Expectations
+In addition to the informal descriptions below, we've also written down a more formal list of expectations that come with each staff role. While this list is mostly for internal use, you can read it [here](/pages/server-info/staff-role-expectations/).
+
+### <span class="fas fa-circle" style="color:#f85950"></span> Owners
+**Description:** Owners of the server.
+
+### <span class="fas fa-circle" style="color:#ff784d"></span> Admins
+**Description:** Staff who administrate the server, its function, its staff, and are involved in deciding the direction of the server.
+
+### <span class="fas fa-circle" style="color:#1abc9c"></span> Domain Leads
+**Description:** Staff in charge of a certain domain such as moderation, events, and outreach. A lead will have a second role specifying their domain.
+
+### <span class="fas fa-circle" style="color:#8dc2ba"></span> Project Leads
+**Description:** Staff in charge of a certain project that require special attention, such as a YouTube video series or our new forms page.
+
+### <span class="fas fa-circle" style="color:#ff9f1b"></span> Moderators
+**Description:** Staff who moderate the server, enforce the rules, and coordinate with staff to support the server.
+
+### <span class="fas fa-circle" style="color:#a1d1ff"></span> PyDis Core Developers
+**Description:** A role for staff who are critical contributors to the server's core projects, like the [bot](https://github.com/python-discord/bot) and the [site](https://github.com/python-discord/site), and are in charge of managing the repositories.
+
+### <span class="fas fa-circle" style="color:#a1d1ff"></span> DevOps
+**Description:** A role for staff involved with the DevOps toolchain of our core projects.
+
+### <span class="fas fa-circle" style="color:#f8d188"></span> Project Teams
+**Description:** Staff can join teams which work on specific projects in the organisation, such as our code jams, media projects, and more.
+
+### <span class="fas fa-circle" style="color:#eecd36"></span> Helpers
+**Description:** This is the core staff role in our organization: All staff members have the Helpers role.
+
+In general, being a helper means that you provide substantial help for the server's function, and have a good understanding of the culture and rules of the server.
+
+Helpers assist in the help channels, demonstrate proficiency in the language, and have strong teaching and explanation skills.
+Otherwise they might assist in other areas of the organization, such as being a core developer, events team member, or moderator.
+
+Being a helper is also more than just quantity of messages, it's about quality. We watch and we pick these people out of the crowd, because we believe that they're a valuable asset to the community, and want our users to know that they're someone that can be relied on for answers and help.
+
+---
+
+# Code Jam Roles
+### <span class="fas fa-circle" style="color:#f87dc8"></span> Code Jam Champions
+**Description:** A vanity role for winners of past code jams.
+
+**How to get it:** Win a code jam!
+
+
+### <span class="fas fa-circle" style="color:#28866c"></span> Code Jam Leaders
+**Description:** A temporary role for the duration of a code jam given to team leaders.
+
+**How to get it:** Team leaders are picked from the participants by the Events Team, and assigned for the duration of a jam.
+
+
+### <span class="fas fa-circle" style="color:#229939"></span> Code Jam Participants
+**Description:** A temporary role for the duration of a code jam given to participants.
+
+**How to get it:** Qualify for and participate in a code jam.
+
+*Note: Similar roles may exist for a game jam.*
+
+
+---
+
+# Miscellaneous Roles
+
+### <span class="fas fa-circle" style="color:#9f3fee"></span> Partners
+**Description:** Representatives of communities we are partnered with. For a list of partnered communities, see the `#partners` channel.
+
+*Note: Not related to [Discord Partners](https://discordapp.com/partners), which our server is currently a part of.*
+
+### <span class="fas fa-circle" style="color:#c77cfa"></span> Python Community
+**Description:** Prominent people in the Python ecosystem.
+Typically this will be people who have written books, people who speak at PyCon, YouTube content creators, podcasters, or notable contributors to a Python runtime or a major Python module.
+These members will have a meta role attached to further explain why they have this role, for example `CPython: Core Developer`.
diff --git a/pydis_site/apps/content/resources/server-info/staff-role-expectations.md b/pydis_site/apps/content/resources/server-info/staff-role-expectations.md
new file mode 100644
index 00000000..286386d7
--- /dev/null
+++ b/pydis_site/apps/content/resources/server-info/staff-role-expectations.md
@@ -0,0 +1,67 @@
+---
+title: Staff Role Expectations
+description: List of expectations that come with being on the staff team at Python Discord.
+icon: fab fa-discord
+---
+
+This page has a list of expectations that come with having a certain staff role in our community.
+While the term "expectations" may sound a bit formal, it's important to keep in mind that everyone with a staff role is just a volunteer and that this list is a way of having a clear overview of what each role entails.
+
+This document is mostly meant for internal reference.
+If you want a more informal description of each staff role, take a look at our [roles page](/pages/server-info/roles/#staff-roles).
+
+## Expectations
+
+### <span class="fas fa-circle" style="color:#eecd36"></span> Helpers
+
+* In general, helpers participate in Python-related channels (e.g. Help Channels, Topical Channels) and help other members of our community.
+* Helpers may also help the community by taking up organizational tasks.
+* There are no real requirements for the level of activity a helper has to have, although we do expect their activity level to be more than "nothing".
+
+### <span class="fas fa-circle" style="color:#ff9f1b"></span> Moderators
+
+* Moderators moderate our community and participate in moderation discussions in our moderators channel.
+* While moderators don't need to have high levels of activity, we do expect some form of consistent activity.
+This means that consistently being active a few times a month is better than having one day with a lot of activity per year.
+Having some kind of consistent activity helps moderators bond with the rest of the moderation team and helps them to stay up to date with the moderation policy.
+* **Moderators are not required to fulfill the helper criteria in addition to this,** although it's is obviously appreciated if they do.
+
+### <span class="fas fa-circle" style="color:#ff784d"></span> Admins
+
+* Admins are expected to work on tasks that directly improve the community on a regular basis.
+* Examples of these tasks include:
+ * Doing pull request reviews;
+ * Being involved in events;
+ * Overseeing road map items;
+ * Solving critical issues;
+ * Handling raids;
+ * Joining our meetings (if in a compatible timezone);
+ * Actioning issues on the organisation repo;
+ * Improving our infrastructure;
+ * Writing documentation or guides;
+ * Recruiting and on-boarding new staff members;
+ * Calling staff votes for nominees;
+ * Having one-on-ones with moderators.
+* Admins are also expected to keep each other updated on the status of the tasks they are working on.
+
+### <span class="fas fa-circle" style="color:#f85950"></span> Owners
+
+**In addition to** the regular Admin criteria, Owners also have to:
+
+* Join staff/admin meetings as often as possible and lead those meetings.
+* Help identify the most critical tasks and try to distribute them among the Admins during the weekly Admin meeting.
+* Make sure that no one is "blocked" in performing their tasks.
+* Ensure that the community isn’t neglecting important responsibilities.
+* Manage partnerships, sponsorships and speak on behalf of the community in public settings.
+
+---
+
+## Staff Management
+First of all, it's important to appreciate that everything staff members do in this community is voluntary and the expectations listed above are not meant to change that.
+**This means it's absolutely fine for all staff members to take breaks or vacations from their activities in the community when they need to.**
+We will never hold it against someone if they are temporarily away from their responsibilities.
+
+At the same time, it's only natural for a community like ours that there's some amount of staff turnover as personal interests and circumstances change.
+Going forward, we will periodically review the activity of individual staff members and open a dialogue with staff members who are currently not meeting the expectations to see what can be done.
+It might happen that we come to conclusion that it's better for a staff member to step down from their current position.
+Do note that there are no hard feelings involved if that happens; we just want to make sure that the current staffing reflects the people who are still interested in volunteering in this community.
diff --git a/pydis_site/apps/content/tests/__init__.py b/pydis_site/apps/content/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/pydis_site/apps/content/tests/__init__.py
diff --git a/pydis_site/apps/content/tests/helpers.py b/pydis_site/apps/content/tests/helpers.py
new file mode 100644
index 00000000..29140375
--- /dev/null
+++ b/pydis_site/apps/content/tests/helpers.py
@@ -0,0 +1,91 @@
+from pyfakefs.fake_filesystem_unittest import TestCase
+
+# Valid markdown content with YAML metadata
+MARKDOWN_WITH_METADATA = """
+---
+title: TestTitle
+description: TestDescription
+relevant_links:
+ Python Discord: https://pythondiscord.com
+ Discord: https://discord.com
+toc: 0
+---
+# This is a header.
+"""
+
+MARKDOWN_WITHOUT_METADATA = """#This is a header."""
+
+# Valid YAML in a _info.yml file
+CATEGORY_INFO = """
+title: Category Name
+description: Description
+"""
+
+# The HTML generated from the above markdown data
+PARSED_HTML = (
+ '<h1 id="this-is-a-header">This is a header.'
+ '<a class="headerlink" href="#this-is-a-header" title="Permanent link">&para;</a></h1>'
+)
+
+# The YAML metadata parsed from the above markdown data
+PARSED_METADATA = {
+ "title": "TestTitle", "description": "TestDescription",
+ "relevant_links": {
+ "Python Discord": "https://pythondiscord.com",
+ "Discord": "https://discord.com"
+ },
+ "toc": 0
+}
+
+# The YAML data parsed from the above _info.yml file
+PARSED_CATEGORY_INFO = {"title": "Category Name", "description": "Description"}
+
+
+class MockPagesTestCase(TestCase):
+ """
+ TestCase with a fake filesystem for testing.
+
+ Structure:
+ ├── _info.yml
+ ├── root.md
+ ├── root_without_metadata.md
+ ├── not_a_page.md
+ ├── tmp.md
+ ├── tmp
+ |   ├── _info.yml
+ |   └── category
+ |    ├── _info.yml
+ |      └── subcategory_without_info
+ └── category
+    ├── _info.yml
+    ├── with_metadata.md
+    └── subcategory
+    ├── with_metadata.md
+       └── without_metadata.md
+ """
+
+ def setUp(self):
+ """Create the fake filesystem."""
+ self.setUpPyfakefs()
+
+ self.fs.create_file("_info.yml", contents=CATEGORY_INFO)
+ self.fs.create_file("root.md", contents=MARKDOWN_WITH_METADATA)
+ self.fs.create_file("root_without_metadata.md", contents=MARKDOWN_WITHOUT_METADATA)
+ self.fs.create_file("not_a_page.md/_info.yml", contents=CATEGORY_INFO)
+ self.fs.create_file("category/_info.yml", contents=CATEGORY_INFO)
+ self.fs.create_file("category/with_metadata.md", contents=MARKDOWN_WITH_METADATA)
+ self.fs.create_file("category/subcategory/_info.yml", contents=CATEGORY_INFO)
+ self.fs.create_file(
+ "category/subcategory/with_metadata.md", contents=MARKDOWN_WITH_METADATA
+ )
+ self.fs.create_file(
+ "category/subcategory/without_metadata.md", contents=MARKDOWN_WITHOUT_METADATA
+ )
+
+ # There is always a `tmp` directory in the filesystem, so make it a category
+ # for testing purposes.
+ # See: https://jmcgeheeiv.github.io/pyfakefs/release/usage.html#os-temporary-directories
+ self.fs.create_file("tmp/_info.yml", contents=CATEGORY_INFO)
+ self.fs.create_file("tmp.md", contents=MARKDOWN_WITH_METADATA)
+ self.fs.create_file("tmp/category/_info.yml", contents=CATEGORY_INFO)
+ self.fs.create_dir("tmp/category/subcategory_without_info")
diff --git a/pydis_site/apps/content/tests/test_utils.py b/pydis_site/apps/content/tests/test_utils.py
new file mode 100644
index 00000000..6612e44c
--- /dev/null
+++ b/pydis_site/apps/content/tests/test_utils.py
@@ -0,0 +1,93 @@
+from pathlib import Path
+
+from django.http import Http404
+
+from pydis_site.apps.content import utils
+from pydis_site.apps.content.tests.helpers import (
+ MockPagesTestCase, PARSED_CATEGORY_INFO, PARSED_HTML, PARSED_METADATA
+)
+
+
+class GetCategoryTests(MockPagesTestCase):
+ """Tests for the get_category function."""
+
+ def test_get_valid_category(self):
+ result = utils.get_category(Path("category"))
+
+ self.assertEqual(result, {"title": "Category Name", "description": "Description"})
+
+ def test_get_nonexistent_category(self):
+ with self.assertRaises(Http404):
+ utils.get_category(Path("invalid"))
+
+ def test_get_category_with_path_to_file(self):
+ # Valid categories are directories, not files
+ with self.assertRaises(Http404):
+ utils.get_category(Path("root.md"))
+
+ def test_get_category_without_info_yml(self):
+ # Categories should provide an _info.yml file
+ with self.assertRaises(FileNotFoundError):
+ utils.get_category(Path("tmp/category/subcategory_without_info"))
+
+
+class GetCategoriesTests(MockPagesTestCase):
+ """Tests for the get_categories function."""
+
+ def test_get_root_categories(self):
+ result = utils.get_categories(Path("."))
+
+ info = PARSED_CATEGORY_INFO
+ self.assertEqual(result, {"category": info, "tmp": info, "not_a_page.md": info})
+
+ def test_get_categories_with_subcategories(self):
+ result = utils.get_categories(Path("category"))
+
+ self.assertEqual(result, {"subcategory": PARSED_CATEGORY_INFO})
+
+ def test_get_categories_without_subcategories(self):
+ result = utils.get_categories(Path("category/subcategory"))
+
+ self.assertEqual(result, {})
+
+
+class GetCategoryPagesTests(MockPagesTestCase):
+ """Tests for the get_category_pages function."""
+
+ def test_get_pages_in_root_category_successfully(self):
+ """The method should successfully retrieve page metadata."""
+ root_category_pages = utils.get_category_pages(Path("."))
+ self.assertEqual(
+ root_category_pages, {"root": PARSED_METADATA, "root_without_metadata": {}}
+ )
+
+ def test_get_pages_in_subcategories_successfully(self):
+ """The method should successfully retrieve page metadata."""
+ category_pages = utils.get_category_pages(Path("category"))
+
+ # Page metadata is properly retrieved
+ self.assertEqual(category_pages, {"with_metadata": PARSED_METADATA})
+
+
+class GetPageTests(MockPagesTestCase):
+ """Tests for the get_page function."""
+
+ def test_get_page(self):
+ # TOC is a special case because the markdown converter outputs the TOC as HTML
+ updated_metadata = {**PARSED_METADATA, "toc": '<div class="toc">\n<ul></ul>\n</div>\n'}
+ cases = [
+ ("Root page with metadata", "root.md", PARSED_HTML, updated_metadata),
+ ("Root page without metadata", "root_without_metadata.md", PARSED_HTML, {}),
+ ("Page with metadata", "category/with_metadata.md", PARSED_HTML, updated_metadata),
+ ("Page without metadata", "category/subcategory/without_metadata.md", PARSED_HTML, {}),
+ ]
+
+ for msg, page_path, expected_html, expected_metadata in cases:
+ with self.subTest(msg=msg):
+ html, metadata = utils.get_page(Path(page_path))
+ self.assertEqual(html, expected_html)
+ self.assertEqual(metadata, expected_metadata)
+
+ def test_get_nonexistent_page_returns_404(self):
+ with self.assertRaises(Http404):
+ utils.get_page(Path("invalid"))
diff --git a/pydis_site/apps/content/tests/test_views.py b/pydis_site/apps/content/tests/test_views.py
new file mode 100644
index 00000000..74d38f78
--- /dev/null
+++ b/pydis_site/apps/content/tests/test_views.py
@@ -0,0 +1,184 @@
+from pathlib import Path
+from unittest import TestCase
+
+from django.http import Http404
+from django.test import RequestFactory, SimpleTestCase, override_settings
+from pyfakefs import fake_filesystem_unittest
+
+from pydis_site.apps.content.tests.helpers import (
+ MockPagesTestCase, PARSED_CATEGORY_INFO, PARSED_HTML, PARSED_METADATA
+)
+from pydis_site.apps.content.views import PageOrCategoryView
+
+
+# Set the module constant within Patcher to use the fake filesystem
+# https://jmcgeheeiv.github.io/pyfakefs/master/usage.html#modules-to-reload
+with fake_filesystem_unittest.Patcher() as _:
+ BASE_PATH = Path(".")
+
+
+def patch_dispatch_attributes(view: PageOrCategoryView, location: str) -> None:
+ """
+ Set the attributes set in the `dispatch` method manually.
+
+ This is necessary because it is never automatically called during tests.
+ """
+ view.location = Path(location)
+
+ # URL location on the filesystem
+ view.full_location = view.location
+
+ # Possible places to find page content information
+ view.category_path = view.full_location
+ view.page_path = view.full_location.with_suffix(".md")
+
+
+@override_settings(CONTENT_PAGES_PATH=BASE_PATH)
+class PageOrCategoryViewTests(MockPagesTestCase, SimpleTestCase, TestCase):
+ """Tests for the PageOrCategoryView class."""
+
+ def setUp(self):
+ """Set test helpers, then set up fake filesystem."""
+ self.factory = RequestFactory()
+ self.view = PageOrCategoryView.as_view()
+ self.ViewClass = PageOrCategoryView()
+ super().setUp()
+
+ # Integration tests
+ def test_valid_page_or_category_returns_200(self):
+ cases = [
+ ("Page at root", "root"),
+ ("Category page", "category"),
+ ("Page in category", "category/with_metadata"),
+ ("Subcategory page", "category/subcategory"),
+ ("Page in subcategory", "category/subcategory/with_metadata"),
+ ]
+ for msg, path in cases:
+ with self.subTest(msg=msg, path=path):
+ request = self.factory.get(f"/{path}")
+ response = self.view(request, location=path)
+ self.assertEqual(response.status_code, 200)
+
+ def test_nonexistent_page_returns_404(self):
+ with self.assertRaises(Http404):
+ request = self.factory.get("/invalid")
+ self.view(request, location="invalid")
+
+ # Unit tests
+ def test_get_template_names_returns_correct_templates(self):
+ category_template = "content/listing.html"
+ page_template = "content/page.html"
+ cases = [
+ ("root", page_template),
+ ("root_without_metadata", page_template),
+ ("category/with_metadata", page_template),
+ ("category/subcategory/with_metadata", page_template),
+ ("category", category_template),
+ ("category/subcategory", category_template),
+ ]
+
+ for path, expected_template in cases:
+ with self.subTest(path=path, expected_template=expected_template):
+ patch_dispatch_attributes(self.ViewClass, path)
+ self.assertEqual(self.ViewClass.get_template_names(), [expected_template])
+
+ def test_get_template_names_with_nonexistent_paths_returns_404(self):
+ for path in ("invalid", "another_invalid", "nonexistent"):
+ with self.subTest(path=path):
+ patch_dispatch_attributes(self.ViewClass, path)
+ with self.assertRaises(Http404):
+ self.ViewClass.get_template_names()
+
+ def test_get_template_names_returns_page_template_for_category_with_page(self):
+ """Make sure the proper page is returned for category locations with pages."""
+ patch_dispatch_attributes(self.ViewClass, "tmp")
+ self.assertEqual(self.ViewClass.get_template_names(), ["content/page.html"])
+
+ def test_get_context_data_with_valid_page(self):
+ """The method should return required fields in the template context."""
+ request = self.factory.get("/root")
+ self.ViewClass.setup(request)
+ self.ViewClass.dispatch(request, location="root")
+
+ cases = [
+ ("Context includes HTML page content", "page", PARSED_HTML),
+ ("Context includes page title", "page_title", PARSED_METADATA["title"]),
+ (
+ "Context includes page description",
+ "page_description",
+ PARSED_METADATA["description"]
+ ),
+ (
+ "Context includes relevant link names and URLs",
+ "relevant_links",
+ PARSED_METADATA["relevant_links"]
+ ),
+ ]
+ context = self.ViewClass.get_context_data()
+ for msg, key, expected_value in cases:
+ with self.subTest(msg=msg):
+ self.assertEqual(context[key], expected_value)
+
+ def test_get_context_data_with_valid_category(self):
+ """The method should return required fields in the template context."""
+ request = self.factory.get("/category")
+ self.ViewClass.setup(request)
+ self.ViewClass.dispatch(request, location="category")
+
+ cases = [
+ (
+ "Context includes subcategory names and their information",
+ "categories",
+ {"subcategory": PARSED_CATEGORY_INFO}
+ ),
+ (
+ "Context includes page names and their metadata",
+ "pages",
+ {"with_metadata": PARSED_METADATA}
+ ),
+ (
+ "Context includes page description",
+ "page_description",
+ PARSED_CATEGORY_INFO["description"]
+ ),
+ ("Context includes page title", "page_title", PARSED_CATEGORY_INFO["title"]),
+ ]
+
+ context = self.ViewClass.get_context_data()
+ for msg, key, expected_value in cases:
+ with self.subTest(msg=msg):
+ self.assertEqual(context[key], expected_value)
+
+ def test_get_context_data_for_category_with_page(self):
+ """Make sure the proper page is returned for category locations with pages."""
+ request = self.factory.get("/category")
+ self.ViewClass.setup(request)
+ self.ViewClass.dispatch(request, location="tmp")
+
+ context = self.ViewClass.get_context_data()
+ expected_page_context = {
+ "page": PARSED_HTML,
+ "page_title": PARSED_METADATA["title"],
+ "page_description": PARSED_METADATA["description"],
+ "relevant_links": PARSED_METADATA["relevant_links"],
+ "subarticles": [{"path": "category", "name": "Category Name"}]
+ }
+ for key, expected_value in expected_page_context.items():
+ with self.subTest():
+ self.assertEqual(context[key], expected_value)
+
+ def test_get_context_data_breadcrumbs(self):
+ """The method should return correct breadcrumbs."""
+ request = self.factory.get("/category/subcategory/with_metadata")
+ self.ViewClass.setup(request)
+ self.ViewClass.dispatch(request, location="category/subcategory/with_metadata")
+
+ context = self.ViewClass.get_context_data()
+ self.assertEquals(
+ context["breadcrumb_items"],
+ [
+ {"name": PARSED_CATEGORY_INFO["title"], "path": "."},
+ {"name": PARSED_CATEGORY_INFO["title"], "path": "category"},
+ {"name": PARSED_CATEGORY_INFO["title"], "path": "category/subcategory"},
+ ]
+ )
diff --git a/pydis_site/apps/content/urls.py b/pydis_site/apps/content/urls.py
new file mode 100644
index 00000000..c11b222a
--- /dev/null
+++ b/pydis_site/apps/content/urls.py
@@ -0,0 +1,9 @@
+from django.urls import path
+
+from . import views
+
+app_name = "content"
+urlpatterns = [
+ path("", views.PageOrCategoryView.as_view(), name='pages'),
+ path("<path:location>/", views.PageOrCategoryView.as_view(), name='page_category'),
+]
diff --git a/pydis_site/apps/content/utils.py b/pydis_site/apps/content/utils.py
new file mode 100644
index 00000000..d3f270ff
--- /dev/null
+++ b/pydis_site/apps/content/utils.py
@@ -0,0 +1,64 @@
+from pathlib import Path
+from typing import Dict, Tuple
+
+import frontmatter
+import markdown
+import yaml
+from django.http import Http404
+from markdown.extensions.toc import TocExtension
+
+
+def get_category(path: Path) -> Dict[str, str]:
+ """Load category information by name from _info.yml."""
+ if not path.is_dir():
+ raise Http404("Category not found.")
+
+ return yaml.safe_load(path.joinpath("_info.yml").read_text(encoding="utf-8"))
+
+
+def get_categories(path: Path) -> Dict[str, Dict]:
+ """Get information for all categories."""
+ categories = {}
+
+ for item in path.iterdir():
+ if item.is_dir():
+ categories[item.name] = get_category(item)
+
+ return categories
+
+
+def get_category_pages(path: Path) -> Dict[str, Dict]:
+ """Get all page names and their metadata at a category path."""
+ pages = {}
+
+ for item in path.glob("*.md"):
+ # Only list page if there is no category with the same name
+ if item.is_file() and not item.with_suffix("").is_dir():
+ pages[item.stem] = frontmatter.load(item).metadata
+
+ return pages
+
+
+def get_page(path: Path) -> Tuple[str, Dict]:
+ """Get one specific page."""
+ if not path.is_file():
+ raise Http404("Page not found.")
+
+ metadata, content = frontmatter.parse(path.read_text(encoding="utf-8"))
+ toc_depth = metadata.get("toc", 1)
+
+ md = markdown.Markdown(
+ extensions=[
+ "extra",
+ # Empty string for marker to disable text searching for [TOC]
+ # By using a metadata key instead, we save time on long markdown documents
+ TocExtension(permalink=True, marker="", toc_depth=toc_depth)
+ ]
+ )
+ html = md.convert(content)
+
+ # Don't set the TOC if the metadata does not specify one
+ if "toc" in metadata:
+ metadata["toc"] = md.toc
+
+ return str(html), metadata
diff --git a/pydis_site/apps/content/views/__init__.py b/pydis_site/apps/content/views/__init__.py
new file mode 100644
index 00000000..70ea1c7a
--- /dev/null
+++ b/pydis_site/apps/content/views/__init__.py
@@ -0,0 +1,3 @@
+from .page_category import PageOrCategoryView
+
+__all__ = ["PageOrCategoryView"]
diff --git a/pydis_site/apps/content/views/page_category.py b/pydis_site/apps/content/views/page_category.py
new file mode 100644
index 00000000..5af77aff
--- /dev/null
+++ b/pydis_site/apps/content/views/page_category.py
@@ -0,0 +1,95 @@
+import typing as t
+from pathlib import Path
+
+import frontmatter
+from django.conf import settings
+from django.http import Http404
+from django.views.generic import TemplateView
+
+from pydis_site.apps.content import utils
+
+
+class PageOrCategoryView(TemplateView):
+ """Handles pages and page categories."""
+
+ def dispatch(self, request: t.Any, *args, **kwargs) -> t.Any:
+ """Conform URL path location to the filesystem path."""
+ self.location = Path(kwargs.get("location", ""))
+
+ # URL location on the filesystem
+ self.full_location = settings.CONTENT_PAGES_PATH / self.location
+
+ # Possible places to find page content information
+ self.category_path = self.full_location
+ self.page_path = self.full_location.with_suffix(".md")
+
+ return super().dispatch(request, *args, **kwargs)
+
+ def get_template_names(self) -> t.List[str]:
+ """Checks if the view uses the page template or listing template."""
+ if self.page_path.is_file():
+ template_name = "content/page.html"
+ elif self.category_path.is_dir():
+ template_name = "content/listing.html"
+ else:
+ raise Http404
+
+ return [template_name]
+
+ def get_context_data(self, **kwargs) -> t.Dict[str, t.Any]:
+ """Assign proper context variables based on what resource user requests."""
+ context = super().get_context_data(**kwargs)
+
+ if self.page_path.is_file():
+ context.update(self._get_page_context(self.page_path))
+ elif self.category_path.is_dir():
+ context.update(self._get_category_context(self.category_path))
+ context["path"] = f"{self.location}/" # Add trailing slash to simplify template
+ else:
+ raise Http404
+
+ # Add subarticle information for dropdown menu if the page is also a category
+ if self.page_path.is_file() and self.category_path.is_dir():
+ context["subarticles"] = []
+ for entry in self.category_path.iterdir():
+ entry_info = {"path": entry.stem}
+ if entry.suffix == ".md" and not entry.with_suffix("").is_dir():
+ entry_info["name"] = frontmatter.load(entry).metadata["title"]
+ elif entry.is_dir():
+ entry_info["name"] = utils.get_category(entry)["title"]
+ else: # pragma: no cover
+ # TODO: Remove coverage.py pragma in Python 3.10
+ # See: https://github.com/nedbat/coveragepy/issues/198
+ continue
+ context["subarticles"].append(entry_info)
+
+ context["breadcrumb_items"] = [
+ {
+ "name": utils.get_category(settings.CONTENT_PAGES_PATH / location)["title"],
+ "path": str(location)
+ } for location in reversed(self.location.parents)
+ ]
+
+ return context
+
+ @staticmethod
+ def _get_page_context(path: Path) -> t.Dict[str, t.Any]:
+ page, metadata = utils.get_page(path)
+ return {
+ "page": page,
+ "page_title": metadata["title"],
+ "page_description": metadata["description"],
+ "relevant_links": metadata.get("relevant_links", {}),
+ "toc": metadata.get("toc")
+ }
+
+ @staticmethod
+ def _get_category_context(path: Path) -> t.Dict[str, t.Any]:
+ category = utils.get_category(path)
+ return {
+ "categories": utils.get_categories(path),
+ "pages": utils.get_category_pages(path),
+ "page_title": category["title"],
+ "page_description": category["description"],
+ "icon": category.get("icon"),
+ }
diff --git a/pydis_site/apps/events/__init__.py b/pydis_site/apps/events/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/pydis_site/apps/events/__init__.py
diff --git a/pydis_site/apps/events/apps.py b/pydis_site/apps/events/apps.py
new file mode 100644
index 00000000..a1cf09ef
--- /dev/null
+++ b/pydis_site/apps/events/apps.py
@@ -0,0 +1,7 @@
+from django.apps import AppConfig
+
+
+class EventsConfig(AppConfig):
+ """Django AppConfig for events app."""
+
+ name = 'events'
diff --git a/pydis_site/apps/events/migrations/__init__.py b/pydis_site/apps/events/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/pydis_site/apps/events/migrations/__init__.py
diff --git a/pydis_site/apps/events/tests/__init__.py b/pydis_site/apps/events/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/pydis_site/apps/events/tests/__init__.py
diff --git a/pydis_site/apps/events/tests/test_views.py b/pydis_site/apps/events/tests/test_views.py
new file mode 100644
index 00000000..23c9e596
--- /dev/null
+++ b/pydis_site/apps/events/tests/test_views.py
@@ -0,0 +1,42 @@
+from pathlib import Path
+
+from django.conf import settings
+from django.test import TestCase, override_settings
+from django_hosts.resolvers import reverse
+
+
+PAGES_PATH = Path(settings.BASE_DIR, "pydis_site", "templates", "events", "test-pages")
+
+
+class IndexTests(TestCase):
+ def test_events_index_response_200(self):
+ """Should return response code 200 when visiting index of events."""
+ url = reverse("events:index")
+ resp = self.client.get(url)
+ self.assertEqual(resp.status_code, 200)
+
+
+class PageTests(TestCase):
+ @override_settings(EVENTS_PAGES_PATH=PAGES_PATH)
+ def test_valid_event_page_reponse_200(self):
+ """Should return response code 200 when visiting valid event page."""
+ pages = (
+ reverse("events:page", ("my-event",)),
+ reverse("events:page", ("my-event/subpage",)),
+ )
+ for page in pages:
+ with self.subTest(page=page):
+ resp = self.client.get(page)
+ self.assertEqual(resp.status_code, 200)
+
+ @override_settings(EVENTS_PAGES_PATH=PAGES_PATH)
+ def test_invalid_event_page_404(self):
+ """Should return response code 404 when visiting invalid event page."""
+ pages = (
+ reverse("events:page", ("invalid",)),
+ reverse("events:page", ("invalid/invalid",))
+ )
+ for page in pages:
+ with self.subTest(page=page):
+ resp = self.client.get(page)
+ self.assertEqual(resp.status_code, 404)
diff --git a/pydis_site/apps/events/urls.py b/pydis_site/apps/events/urls.py
new file mode 100644
index 00000000..9a65cf1f
--- /dev/null
+++ b/pydis_site/apps/events/urls.py
@@ -0,0 +1,9 @@
+from django.urls import path
+
+from pydis_site.apps.events.views import IndexView, PageView
+
+app_name = "events"
+urlpatterns = [
+ path("", IndexView.as_view(), name="index"),
+ path("<path:path>/", PageView.as_view(), name="page"),
+]
diff --git a/pydis_site/apps/events/views/__init__.py b/pydis_site/apps/events/views/__init__.py
new file mode 100644
index 00000000..8a107e2f
--- /dev/null
+++ b/pydis_site/apps/events/views/__init__.py
@@ -0,0 +1,4 @@
+from .index import IndexView
+from .page import PageView
+
+__all__ = ["IndexView", "PageView"]
diff --git a/pydis_site/apps/events/views/index.py b/pydis_site/apps/events/views/index.py
new file mode 100644
index 00000000..7ffba74a
--- /dev/null
+++ b/pydis_site/apps/events/views/index.py
@@ -0,0 +1,7 @@
+from django.views.generic import TemplateView
+
+
+class IndexView(TemplateView):
+ """Events index page view."""
+
+ template_name = "events/index.html"
diff --git a/pydis_site/apps/events/views/page.py b/pydis_site/apps/events/views/page.py
new file mode 100644
index 00000000..1622ad70
--- /dev/null
+++ b/pydis_site/apps/events/views/page.py
@@ -0,0 +1,25 @@
+from typing import List
+
+from django.conf import settings
+from django.http import Http404
+from django.views.generic import TemplateView
+
+
+class PageView(TemplateView):
+ """Handles event pages showing."""
+
+ def get_template_names(self) -> List[str]:
+ """Get specific template names."""
+ path: str = self.kwargs['path']
+ page_path = settings.EVENTS_PAGES_PATH / path
+ if page_path.is_dir():
+ page_path = page_path / "_index.html"
+ path = f"{path}/_index.html"
+ else:
+ page_path = settings.EVENTS_PAGES_PATH / f"{path}.html"
+ path = f"{path}.html"
+
+ if not page_path.exists():
+ raise Http404
+
+ return [f"events/{settings.EVENTS_PAGES_PATH.name}/{path}"]
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/resources/books/byte_of_python.yaml b/pydis_site/apps/home/resources/books/byte_of_python.yaml
deleted file mode 100644
index f3eca902..00000000
--- a/pydis_site/apps/home/resources/books/byte_of_python.yaml
+++ /dev/null
@@ -1,17 +0,0 @@
-description: A free book on programming using the Python language. It serves as a
- tutorial or guide to the Python language for a beginner audience. If all you know
- about computers is how to save text files, then this is the book for you.
-name: A Byte of Python
-payment: optional
-payment_description: A free e-book is available online, a paper version can be bought
- from lulu.com.
-urls:
-- icon: regular/link
- title: E-book
- url: https://python.swaroopch.com/
-- icon: regular/book
- title: Buy the book
- url: http://www.lulu.com/shop/swaroop-c-h/a-byte-of-python/paperback/product-21142968.html
-- icon: regular/tablet-alt
- title: Kindle edition
- url: https://www.amazon.com/Byte-Python-Swaroop-C-H-ebook/dp/B00FJ7S2JU/
diff --git a/pydis_site/apps/home/resources/communities/adafruit.yaml b/pydis_site/apps/home/resources/communities/adafruit.yaml
deleted file mode 100644
index 193f7364..00000000
--- a/pydis_site/apps/home/resources/communities/adafruit.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-description: 'Adafruit is an open-source electronics manufacturer that makes all the
- components you need to start your own Python-powered hardware projects.
-
-
- Their official community host regular show-and-tells, provide help with your projects,
- and the Adafruit devs do all the CircuitPython development right out in the open.
- Join the Maker Revolution today!'
-name: 'Discord: Adafruit'
-payment: free
-payment_description: null
-urls:
-- icon: branding/discord
- title: Adafruit Discord
- url: https://discord.gg/adafruit
diff --git a/pydis_site/apps/home/resources/communities/functional_programming.yaml b/pydis_site/apps/home/resources/communities/functional_programming.yaml
deleted file mode 100644
index ab99f264..00000000
--- a/pydis_site/apps/home/resources/communities/functional_programming.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-description: Functional Programming is a server for discussing functional languages
- like Haskell, Idris, Elixir and Lisp as well as related academic fields such as
- type theory, category theory, proof assistants, and more!
-name: 'Discord: Functional Programming'
-payment: free
-payment_description: null
-urls:
-- icon: branding/discord
- title: Functional Programming Discord
- url: https://discord.gg/kWJYurV
diff --git a/pydis_site/apps/home/resources/communities/pallets.yaml b/pydis_site/apps/home/resources/communities/pallets.yaml
deleted file mode 100644
index e5a18983..00000000
--- a/pydis_site/apps/home/resources/communities/pallets.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-description: The Pallets Projects develop Python libraries such as the Flask web framework,
- the Jinja templating library, and the Click command line toolkit. Join to discuss
- and get help from the Pallets community.
-name: 'Discord: The Pallets Project'
-payment: free
-payment_description: null
-urls:
-- icon: branding/discord
- title: The Pallets Project Discord
- url: https://discord.gg/t6rrQZH
diff --git a/pydis_site/apps/home/resources/communities/rlbot.yaml b/pydis_site/apps/home/resources/communities/rlbot.yaml
deleted file mode 100644
index c62e0260..00000000
--- a/pydis_site/apps/home/resources/communities/rlbot.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-description: RLBot is a community of programmers making awesome Rocket League bots.
- They've created a framework that you can use to write bots in a number of languages
- (including Python), and they host regular tournaments where botmakers can pit their
- creations against each other.
-name: 'Discord: RLBot'
-payment: free
-payment_description: null
-urls:
-- icon: branding/discord
- title: RLBot Discord
- url: https://discord.gg/4JJdJKb
diff --git a/pydis_site/apps/home/resources/communities/subreddit.yaml b/pydis_site/apps/home/resources/communities/subreddit.yaml
deleted file mode 100644
index 217a84ac..00000000
--- a/pydis_site/apps/home/resources/communities/subreddit.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
-description: News about the Python programming language, and language-related discussion
-name: 'Subreddit: r/Python'
-payment: free
-payment_description: null
-urls:
-- icon: branding/reddit-alien
- title: r/Python on Reddit
- url: https://www.reddit.com/r/Python/
diff --git a/pydis_site/apps/home/resources/editors/atom.yaml b/pydis_site/apps/home/resources/editors/atom.yaml
deleted file mode 100644
index f05e45a3..00000000
--- a/pydis_site/apps/home/resources/editors/atom.yaml
+++ /dev/null
@@ -1,12 +0,0 @@
-description: A free Electron-based editor, a "hackable text editor for the 21st century", maintained
- by the GitHub team.
-name: Atom
-payment: free
-payment_description: null
-urls:
-- icon: regular/link
- title: Website
- url: https://atom.io/
-- icon: branding/github
- title: GitHub
- url: https://github.com/atom/atom
diff --git a/pydis_site/apps/home/resources/editors/mu_editor.yaml b/pydis_site/apps/home/resources/editors/mu_editor.yaml
deleted file mode 100644
index cb44d750..00000000
--- a/pydis_site/apps/home/resources/editors/mu_editor.yaml
+++ /dev/null
@@ -1,12 +0,0 @@
-description: An editor aimed at beginners for the purpose of learning how to code
- without the distractions more advanced editors sometimes cause.
-name: Mu-Editor
-payment: free
-payment_description: null
-urls:
-- icon: regular/link
- title: Website
- url: https://codewith.mu/en/
-- icon: branding/github
- title: GitHub
- url: https://github.com/mu-editor/mu/
diff --git a/pydis_site/apps/home/resources/editors/sublime_text.yaml b/pydis_site/apps/home/resources/editors/sublime_text.yaml
deleted file mode 100644
index 97952d35..00000000
--- a/pydis_site/apps/home/resources/editors/sublime_text.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
-description: A powerful Python-backed editor with great community support and a wealth
- of extensions.
-name: Sublime Text
-payment: optional
-payment_description: Nagware; will ask you to buy the full version after every X saves
-urls:
-- icon: regular/link
- title: Website
- url: https://www.sublimetext.com/
diff --git a/pydis_site/apps/home/resources/editors/visual_studio_code.yaml b/pydis_site/apps/home/resources/editors/visual_studio_code.yaml
deleted file mode 100644
index 4e1f946f..00000000
--- a/pydis_site/apps/home/resources/editors/visual_studio_code.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-description: A fully-featured editor based on Electron, extendable with plugins.
-name: Visual Studio Code
-payment: free
-payment_description: null
-urls:
-- icon: regular/link
- title: Website
- url: https://code.visualstudio.com/
-- icon: branding/github
- title: GitHub
- url: https://github.com/Microsoft/vscode
diff --git a/pydis_site/apps/home/resources/ides/_category_info.yaml b/pydis_site/apps/home/resources/ides/_category_info.yaml
deleted file mode 100644
index d331c95d..00000000
--- a/pydis_site/apps/home/resources/ides/_category_info.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
-description: Fully-integrated development environments for serious Python work
-name: IDEs
diff --git a/pydis_site/apps/home/resources/ides/pycharm.yaml b/pydis_site/apps/home/resources/ides/pycharm.yaml
deleted file mode 100644
index 4624cb41..00000000
--- a/pydis_site/apps/home/resources/ides/pycharm.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-description: The very best Python IDE, with a wealth of advanced features and convenience
- functions.
-name: PyCharm
-payment: optional
-payment_description: There's a free Community Edition and a paid-for Professional
- Edition with more features available
-urls:
-- icon: regular/link
- title: Website
- url: https://www.jetbrains.com/pycharm/
diff --git a/pydis_site/apps/home/resources/ides/spyder.yaml b/pydis_site/apps/home/resources/ides/spyder.yaml
deleted file mode 100644
index 146b3549..00000000
--- a/pydis_site/apps/home/resources/ides/spyder.yaml
+++ /dev/null
@@ -1,12 +0,0 @@
-description: The Scientific PYthon Development EnviRonment. Simpler and lighter than
- PyCharm, but still packs a punch.
-name: Spyder
-payment: free
-payment_description: null
-urls:
-- icon: regular/link
- title: Website
- url: https://www.spyder-ide.org/
-- icon: branding/github
- title: GitHub
- url: https://github.com/spyder-ide/spyder
diff --git a/pydis_site/apps/home/resources/ides/thonny.yaml b/pydis_site/apps/home/resources/ides/thonny.yaml
deleted file mode 100644
index d660094b..00000000
--- a/pydis_site/apps/home/resources/ides/thonny.yaml
+++ /dev/null
@@ -1,12 +0,0 @@
-description: A Python IDE specifially aimed at learning programming. Has a lot of
- helpful features to help you understand your code.
-name: Thonny
-payment: free
-payment_description: null
-urls:
-- icon: regular/link
- title: Website
- url: https://thonny.org/
-- icon: branding/github
- title: GitHub
- url: https://github.com/thonny/thonny/
diff --git a/pydis_site/apps/home/resources/interactive_learning_tools/_category_info.yaml b/pydis_site/apps/home/resources/interactive_learning_tools/_category_info.yaml
deleted file mode 100644
index 08501627..00000000
--- a/pydis_site/apps/home/resources/interactive_learning_tools/_category_info.yaml
+++ /dev/null
@@ -1,3 +0,0 @@
-description: Learn Python with interactive content like courses, games and programming
- challenges.
-name: Interactive Learning Tools
diff --git a/pydis_site/apps/home/resources/interactive_learning_tools/automate_the_boring_stuff.yaml b/pydis_site/apps/home/resources/interactive_learning_tools/automate_the_boring_stuff.yaml
deleted file mode 100644
index 02d76b3b..00000000
--- a/pydis_site/apps/home/resources/interactive_learning_tools/automate_the_boring_stuff.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-description: The interactive course version of Al Sweigart's excellent book for beginners,
- taught by the author himself. This link has a discounted version of the course which
- will always cost 10 dollars. Thanks, Al!
-name: Automate the Boring Stuff with Python
-payment: paid
-payment_description: Paid course with a certificate of completion. Some sample videos
- are available for free.
-urls:
-- icon: regular/graduation-cap
- title: Udemy Course
- url: https://www.udemy.com/automate/?couponCode=FOR_LIKE_10_BUCKS
diff --git a/pydis_site/apps/home/resources/interactive_learning_tools/learn_to_program.yaml b/pydis_site/apps/home/resources/interactive_learning_tools/learn_to_program.yaml
deleted file mode 100644
index 265f1644..00000000
--- a/pydis_site/apps/home/resources/interactive_learning_tools/learn_to_program.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-description: A 2-part course that teaches Python. Primarily intended for high school
- students and first-year university students who want to learn programming.
-name: 'University of Toronto: Learn to Program'
-payment: optional
-payment_description: You can pay to enroll for a graded certificate, or choose to
- audit for free.
-urls:
-- icon: regular/graduation-cap
- title: 'Part 1: The Fundamentals'
- url: https://www.coursera.org/learn/learn-to-program
-- icon: regular/graduation-cap
- title: 'Part 2: Crafting Quality Code'
- url: https://www.coursera.org/learn/program-code
diff --git a/pydis_site/apps/home/resources/interactive_learning_tools/mit_python.yaml b/pydis_site/apps/home/resources/interactive_learning_tools/mit_python.yaml
deleted file mode 100644
index 464b8d4a..00000000
--- a/pydis_site/apps/home/resources/interactive_learning_tools/mit_python.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-description: This MITx offering teaches computer science with Python. It covers computational
- thinking, algorithms, data structures and the Python programming language itself.
-name: 'MIT: Introduction to Computer Science and Programming Using Python'
-payment: optional
-payment_description: You can pay to enroll for a graded certificate, or choose to
- take the full course for free.
-urls:
-- icon: regular/graduation-cap
- title: edX Course
- url: https://www.edx.org/course/introduction-computer-science-mitx-6-00-1x-11
diff --git a/pydis_site/apps/home/resources/interactive_learning_tools/programming_for_everybody.yaml b/pydis_site/apps/home/resources/interactive_learning_tools/programming_for_everybody.yaml
deleted file mode 100644
index a6d7abe1..00000000
--- a/pydis_site/apps/home/resources/interactive_learning_tools/programming_for_everybody.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-description: A 5-part specialization course that teaches Python from scratch. The
- course has no pre-requisites and avoids all but the simplest mathematics.
-name: 'University of Michigan: Programming for Everybody'
-payment: optional
-payment_description: You can pay to enroll for a graded certificate and a capstone
- project, or choose to audit for free.
-urls:
-- icon: regular/graduation-cap
- title: Python for Everyone Specialization
- url: https://www.coursera.org/learn/python
diff --git a/pydis_site/apps/home/resources/misc/_category_info.yaml b/pydis_site/apps/home/resources/misc/_category_info.yaml
deleted file mode 100644
index 4fdc4bf7..00000000
--- a/pydis_site/apps/home/resources/misc/_category_info.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
-description: Resources which do not fit into the other categories
-name: Miscellaneous
diff --git a/pydis_site/apps/home/resources/misc/good_first_issue_tag.yaml b/pydis_site/apps/home/resources/misc/good_first_issue_tag.yaml
deleted file mode 100644
index 35d7a8a4..00000000
--- a/pydis_site/apps/home/resources/misc/good_first_issue_tag.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-description: Searching for opportunities to contribute to a Python project? GitHub
- repository maintainers often mark issues appropriate for novice users with the 'Good
- First Issue' tag. These issues can be explored directly on GitHub.
-name: GitHub's 'Good First Issue' Tag
-payment: free
-payment_description: null
-urls:
-- icon: branding/github
- title: GitHub
- url: https://github.com/search?utf8=%E2%9C%93&q=label%3A%22good+first+issue%22+language%3APython+state%3Aopen&type=Issues&ref=advsearch&l=Python&l=
diff --git a/pydis_site/apps/home/resources/misc/python_cheat_sheet.yaml b/pydis_site/apps/home/resources/misc/python_cheat_sheet.yaml
deleted file mode 100644
index 8c82a5a9..00000000
--- a/pydis_site/apps/home/resources/misc/python_cheat_sheet.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
-description: A Python 3 cheat sheet with useful information and tips, as well as common
- pitfalls for beginners. This is a PDF.
-name: Python Cheat Sheet
-payment: free
-payment_description: null
-urls:
-- icon: regular/link
- title: Website
- url: https://perso.limsi.fr/pointal/_media/python:cours:mementopython3-english.pdf
diff --git a/pydis_site/apps/home/resources/podcasts/_category_info.yaml b/pydis_site/apps/home/resources/podcasts/_category_info.yaml
deleted file mode 100644
index a0f9025c..00000000
--- a/pydis_site/apps/home/resources/podcasts/_category_info.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
-description: Notable podcasts about the Python ecosystem
-name: Podcasts
diff --git a/pydis_site/apps/home/resources/tutorials/_category_info.yaml b/pydis_site/apps/home/resources/tutorials/_category_info.yaml
deleted file mode 100644
index a9adc106..00000000
--- a/pydis_site/apps/home/resources/tutorials/_category_info.yaml
+++ /dev/null
@@ -1,3 +0,0 @@
-description: Tutorials and references for those that are just getting started with
- python
-name: Tutorials
diff --git a/pydis_site/apps/home/resources/tutorials/corey_schafer.yaml b/pydis_site/apps/home/resources/tutorials/corey_schafer.yaml
deleted file mode 100644
index 9fff4bbf..00000000
--- a/pydis_site/apps/home/resources/tutorials/corey_schafer.yaml
+++ /dev/null
@@ -1,7 +0,0 @@
-description: An in-depth look at the Python programming language, from one of
- YouTube's most popular Python tutors.
-payment: free
-urls:
- - icon: branding/youtube,
- title: YouTube,
- url: https://www.youtube.com/playlist?list=PL-osiE80TeTt2d9bfVyTiXJA-UTHn6WwU
diff --git a/pydis_site/apps/home/resources/tutorials/get_started_with_flask.yaml b/pydis_site/apps/home/resources/tutorials/get_started_with_flask.yaml
deleted file mode 100644
index 11dd2a4d..00000000
--- a/pydis_site/apps/home/resources/tutorials/get_started_with_flask.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
-description: A fully featured mega-tutorial for learning how to create web applications
- with the Flask framework.
-name: Get Started with Flask Web Development
-payment: free
-payment_description: null
-urls:
-- icon: regular/link
- title: Website
- url: https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world
diff --git a/pydis_site/apps/home/resources/tutorials/getting_started_with_python.yaml b/pydis_site/apps/home/resources/tutorials/getting_started_with_python.yaml
deleted file mode 100644
index 777f2fe3..00000000
--- a/pydis_site/apps/home/resources/tutorials/getting_started_with_python.yaml
+++ /dev/null
@@ -1,12 +0,0 @@
-description: The list of resources for programmers and non-programmers from Python's
- official beginners' guide
-name: Getting Started with Python
-payment: free
-payment_description: null
-urls:
-- icon: regular/link
- title: Beginners Guide for Non-Programmers
- url: https://wiki.python.org/moin/BeginnersGuide/NonProgrammers
-- icon: regular/link
- title: Beginners Guide for Programmers
- url: https://wiki.python.org/moin/BeginnersGuide/Programmers
diff --git a/pydis_site/apps/home/resources/tutorials/hitchhikers_guide_to_python.yaml b/pydis_site/apps/home/resources/tutorials/hitchhikers_guide_to_python.yaml
deleted file mode 100644
index 38eebb56..00000000
--- a/pydis_site/apps/home/resources/tutorials/hitchhikers_guide_to_python.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-description: This opinionated guide exists to provide both novice and expert Python
- developers a best practice handbook to the installation, configuration, and usage
- of Python on a daily basis.
-name: The Hitchhiker's Guide to Python
-payment: free
-payment_description: null
-urls:
-- icon: regular/link
- title: Website
- url: https://python-guide.org/
diff --git a/pydis_site/apps/home/resources/tutorials/sentdex.yaml b/pydis_site/apps/home/resources/tutorials/sentdex.yaml
deleted file mode 100644
index cae2695b..00000000
--- a/pydis_site/apps/home/resources/tutorials/sentdex.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
-description: A Python basics tutorial based around Python 3.7.
-name: Python Tutorials by Sentdex on YouTube
-payment: free
-payment_description: null
-urls:
-- icon: branding/youtube
- title: YouTube
- url: https://www.youtube.com/playlist?list=PLQVvvaa0QuDeAams7fkdcwOGBpGdHpXln
diff --git a/pydis_site/apps/home/resources/tutorials/simple_guide_to_git.yaml b/pydis_site/apps/home/resources/tutorials/simple_guide_to_git.yaml
deleted file mode 100644
index acf76efe..00000000
--- a/pydis_site/apps/home/resources/tutorials/simple_guide_to_git.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
-description: A simple, no-nonsense guide to the basics of using Git.
-name: A Simple Guide to Git
-payment: free
-payment_description: null
-urls:
-- icon: regular/link
- title: Website
- url: http://rogerdudler.github.io/git-guide/
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/templatetags/extra_filters.py b/pydis_site/apps/home/templatetags/extra_filters.py
index d63b3245..89b45831 100644
--- a/pydis_site/apps/home/templatetags/extra_filters.py
+++ b/pydis_site/apps/home/templatetags/extra_filters.py
@@ -11,7 +11,7 @@ def starts_with(value: str, arg: str) -> bool:
Usage:
```django
- {% if request.url | starts_with:"/wiki" %}
+ {% if request.url | starts_with:"/events" %}
...
{% endif %}
```
diff --git a/pydis_site/apps/home/templatetags/wiki_extra.py b/pydis_site/apps/home/templatetags/wiki_extra.py
deleted file mode 100644
index b4b720bf..00000000
--- a/pydis_site/apps/home/templatetags/wiki_extra.py
+++ /dev/null
@@ -1,132 +0,0 @@
-from typing import Any, Dict, List, Type, Union
-
-from django import template
-from django.forms import BooleanField, BoundField, CharField, Field, ImageField, ModelChoiceField
-from django.template import Context
-from django.template.loader import get_template
-from django.utils.safestring import SafeText, mark_safe
-from wiki.editors.markitup import MarkItUpWidget
-from wiki.forms import WikiSlugField
-from wiki.models import URLPath
-from wiki.plugins.notifications.forms import SettingsModelChoiceField
-
-TEMPLATE_PATH = "wiki/forms/fields/{0}.html"
-
-TEMPLATES: Dict[Type, str] = {
- BooleanField: TEMPLATE_PATH.format("boolean"),
- CharField: TEMPLATE_PATH.format("char"),
- ImageField: TEMPLATE_PATH.format("image"),
-
- ModelChoiceField: TEMPLATE_PATH.format("model_choice"),
- SettingsModelChoiceField: TEMPLATE_PATH.format("model_choice"),
- WikiSlugField: TEMPLATE_PATH.format("wiki_slug_render"),
-}
-
-
-register = template.Library()
-
-
-def get_unbound_field(field: Union[BoundField, Field]) -> Field:
- """
- Unwraps a bound Django Forms field, returning the unbound field.
-
- Bound fields often don't give you the same level of access to the field's underlying attributes,
- so sometimes it helps to have access to the underlying field object.
- """
- while isinstance(field, BoundField):
- field = field.field
-
- return field
-
-
-def render(template_path: str, context: Dict[str, Any]) -> SafeText:
- """
- Renders a template at a specified path, with the provided context dictionary.
-
- This was extracted mostly for the sake of mocking it out in the tests - but do note that
- the resulting rendered template is wrapped with `mark_safe`, so it will not be escaped.
- """
- return mark_safe(get_template(template_path).render(context)) # noqa: S703, S308
-
-
-def render_field(field: Field, render_labels: bool = True) -> SafeText:
- """
- Renders a form field using a custom template designed specifically for the wiki forms.
-
- As the wiki uses custom form rendering logic, we were unable to make use of Crispy Forms for
- it. This means that, in order to customize the form fields, we needed to be able to render
- the fields manually. This function handles that logic.
-
- Sometimes we don't want to render the label that goes with a field - the `render_labels`
- argument defaults to True, but can be set to False if the label shouldn't be rendered.
-
- The label rendering logic is left up to the template.
-
- Usage: `{% render_field field_obj [render_labels=True/False] %}`
- """
- unbound_field = get_unbound_field(field)
-
- if not isinstance(render_labels, bool):
- render_labels = True
-
- template_path = TEMPLATES.get(unbound_field.__class__, TEMPLATE_PATH.format("in_place_render"))
- is_markitup = isinstance(unbound_field.widget, MarkItUpWidget)
- context = {"field": field, "is_markitup": is_markitup, "render_labels": render_labels}
-
- return render(template_path, context)
-
-
[email protected]_tag(takes_context=True)
-def get_field_options(context: Context, field: BoundField) -> str:
- """
- Retrieves the field options for a multiple choice field, and stores it in the context.
-
- This tag exists because we can't call functions within Django templates directly, and is
- only made use of in the template for ModelChoice (and derived) fields - but would work fine
- with anything that makes use of your standard `<select>` element widgets.
-
- This stores the parsed options under `options` in the context, which will subsequently
- be available in the template.
-
- Usage:
-
- ```django
- {% get_field_options field_object %}
-
- {% if options %}
- {% for group_name, group_choices, group_index in options %}
- ...
- {% endfor %}
- {% endif %}
- ```
- """
- widget = field.field.widget
-
- if field.value() is None:
- value: List[str] = []
- else:
- value = [str(field.value())]
-
- context["options"] = widget.optgroups(field.name, value)
- return ""
-
-
-def render_urlpath(value: Union[URLPath, str]) -> str:
- """
- Simple filter to render a URLPath (or string) into a template.
-
- This is used where the wiki intends to render a path - mostly because if you just
- `str(url_path)`, you'll actually get a path that starts with `(root)` instead of `/`.
-
- We support strings here as well because the wiki is very inconsistent about when it
- provides a string versus when it provides a URLPath, and it was too much work to figure out
- and account for it in the templates.
-
- Usage: `{{ url_path | render_urlpath }}`
- """
- if isinstance(value, str):
- return value or "/"
-
- return value.path or "/"
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 40c80205..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,29 +8,3 @@ class TestIndexReturns200(TestCase):
url = reverse('home')
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
-
-
-class TestTimelineReturns200(TestCase):
- def test_timeline_returns_200(self):
- """Check that the timeline page returns a HTTP 200 response."""
- url = reverse('timeline')
- 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/tests/test_wiki_templatetags.py b/pydis_site/apps/home/tests/test_wiki_templatetags.py
deleted file mode 100644
index e1e2a02c..00000000
--- a/pydis_site/apps/home/tests/test_wiki_templatetags.py
+++ /dev/null
@@ -1,238 +0,0 @@
-from unittest.mock import Mock, create_autospec
-
-from django.forms import (
- BooleanField, BoundField, CharField, ChoiceField, Field, Form, ImageField,
- ModelChoiceField
-)
-from django.template import Context, Template
-from django.test import TestCase
-from wiki.editors.markitup import MarkItUpWidget
-from wiki.forms import WikiSlugField
-from wiki.models import Article, URLPath as _URLPath
-from wiki.plugins.notifications.forms import SettingsModelChoiceField
-
-from pydis_site.apps.home.templatetags import wiki_extra
-
-URLPath = Mock(_URLPath)
-
-
-class TestURLPathFilter(TestCase):
- TEMPLATE = Template(
- """
- {% load wiki_extra %}
- {{ obj|render_urlpath }}
- """
- )
-
- def test_str(self):
- context = {"obj": "/path/"}
- rendered = self.TEMPLATE.render(Context(context))
-
- self.assertEqual(rendered.strip(), "/path/")
-
- def test_str_empty(self):
- context = {"obj": ""}
- rendered = self.TEMPLATE.render(Context(context))
-
- self.assertEqual(rendered.strip(), "/")
-
- def test_urlpath(self):
- url_path = URLPath()
- url_path.path = "/path/"
-
- context = {"obj": url_path}
- rendered = self.TEMPLATE.render(Context(context))
-
- self.assertEqual(rendered.strip(), "/path/")
-
- def test_urlpath_root(self):
- url_path = URLPath()
- url_path.path = None
-
- context = {"obj": url_path}
- rendered = self.TEMPLATE.render(Context(context))
-
- self.assertEqual(rendered.strip(), "/")
-
-
-class TestRenderField(TestCase):
- TEMPLATE = Template(
- """
- {% load wiki_extra %}
- {% render_field field %}
- """
- )
-
- TEMPLATE_NO_LABELS = Template(
- """
- {% load wiki_extra %}
- {% render_field field render_labels=False %}
- """
- )
-
- TEMPLATE_LABELS_NOT_BOOLEAN = Template(
- """
- {% load wiki_extra %}
- {% render_field field render_labels="" %}
- """
- )
-
- def test_bound_field(self):
- unbound_field = Field()
- field = BoundField(Form(), unbound_field, "field")
-
- context = Context({"field": field})
- self.TEMPLATE.render(context)
-
- def test_bound_field_no_labels(self):
- unbound_field = Field()
- field = BoundField(Form(), unbound_field, "field")
-
- context = Context({"field": field})
- self.TEMPLATE_NO_LABELS.render(context)
-
- def test_bound_field_labels_not_boolean(self):
- unbound_field = Field()
- field = BoundField(Form(), unbound_field, "field")
-
- context = Context({"field": field})
- self.TEMPLATE_LABELS_NOT_BOOLEAN.render(context)
-
- def test_unbound_field(self):
- field = Field()
-
- context = Context({"field": field})
- self.TEMPLATE.render(context)
-
- def test_unbound_field_no_labels(self):
- field = Field()
-
- context = Context({"field": field})
- self.TEMPLATE_NO_LABELS.render(context)
-
- def test_unbound_field_labels_not_boolean(self):
- field = Field()
-
- context = Context({"field": field})
- self.TEMPLATE_LABELS_NOT_BOOLEAN.render(context)
-
-
-class TestRenderFieldTypes(TestCase):
- TEMPLATE = Template(
- """
- {% load wiki_extra %}
- {% render_field field %}
- """
- )
-
- @classmethod
- def setUpClass(cls):
- cls._wiki_extra_render = wiki_extra.render
- wiki_extra.render = create_autospec(wiki_extra.render, return_value="")
-
- @classmethod
- def tearDownClass(cls):
- wiki_extra.render = cls._wiki_extra_render
- del cls._wiki_extra_render
-
- def test_field_boolean(self):
- field = BooleanField()
-
- context = Context({"field": field})
- self.TEMPLATE.render(context)
-
- template_path = "wiki/forms/fields/boolean.html"
- context = {"field": field, "is_markitup": False, "render_labels": True}
-
- wiki_extra.render.assert_called_with(template_path, context)
-
- def test_field_char(self):
- field = CharField()
- field.widget = None
-
- context = Context({"field": field})
- self.TEMPLATE.render(context)
-
- template_path = "wiki/forms/fields/char.html"
- context = {"field": field, "is_markitup": False, "render_labels": True}
-
- wiki_extra.render.assert_called_with(template_path, context)
-
- def test_field_char_markitup(self):
- field = CharField()
- field.widget = MarkItUpWidget()
-
- context = Context({"field": field})
- self.TEMPLATE.render(context)
-
- template_path = "wiki/forms/fields/char.html"
- context = {"field": field, "is_markitup": True, "render_labels": True}
-
- wiki_extra.render.assert_called_with(template_path, context)
-
- def test_field_image(self):
- field = ImageField()
-
- context = Context({"field": field})
- self.TEMPLATE.render(context)
-
- template_path = "wiki/forms/fields/image.html"
- context = {"field": field, "is_markitup": False, "render_labels": True}
-
- wiki_extra.render.assert_called_with(template_path, context)
-
- def test_field_model_choice(self):
- field = ModelChoiceField(Article.objects.all())
-
- context = Context({"field": field})
- self.TEMPLATE.render(context)
-
- template_path = "wiki/forms/fields/model_choice.html"
- context = {"field": field, "is_markitup": False, "render_labels": True}
-
- wiki_extra.render.assert_called_with(template_path, context)
-
- def test_field_settings_model_choice(self):
- field = SettingsModelChoiceField(Article.objects.all())
-
- context = Context({"field": field})
- self.TEMPLATE.render(context)
-
- template_path = "wiki/forms/fields/model_choice.html"
- context = {"field": field, "is_markitup": False, "render_labels": True}
-
- wiki_extra.render.assert_called_with(template_path, context)
-
- def test_field_wiki_slug(self):
- field = WikiSlugField()
-
- context = Context({"field": field})
- self.TEMPLATE.render(context)
-
- template_path = "wiki/forms/fields/wiki_slug_render.html"
- context = {"field": field, "is_markitup": False, "render_labels": True}
-
- wiki_extra.render.assert_called_with(template_path, context)
-
-
-class TestGetFieldOptions(TestCase):
- TEMPLATE = Template(
- """
- {% load wiki_extra %}
- {% get_field_options field %}
- """
- )
-
- def test_get_field_options(self):
- unbound_field = ChoiceField()
- field = BoundField(Form(), unbound_field, "field")
-
- context = Context({"field": field})
- self.TEMPLATE.render(context)
-
- def test_get_field_options_value(self):
- unbound_field = ChoiceField()
- field = BoundField(Form(initial={"field": "Value"}), unbound_field, "field")
-
- context = Context({"field": field})
- self.TEMPLATE.render(context)
diff --git a/pydis_site/apps/home/urls.py b/pydis_site/apps/home/urls.py
index 14d118f8..1e2af8f3 100644
--- a/pydis_site/apps/home/urls.py
+++ b/pydis_site/apps/home/urls.py
@@ -1,42 +1,15 @@
-from allauth.account.views import LogoutView
-from django.conf import settings
-from django.conf.urls.static import static
from django.contrib import admin
-from django.contrib.messages import ERROR
from django.urls import include, path
-from pydis_site.utils.views import MessageRedirectView
-from .views import AccountDeleteView, AccountSettingsView, HomeView, timeline
+from .views import HomeView, timeline
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('pages/', include('wiki.urls')),
-
- 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('', include('pydis_site.apps.redirect.urls')),
path('admin/', admin.site.urls),
- path('notifications/', include('django_nyt.urls')),
+ path('resources/', include('pydis_site.apps.resources.urls')),
+ path('pages/', include('pydis_site.apps.content.urls')),
+ path('events/', include('pydis_site.apps.events.urls', namespace='events')),
path('timeline/', timeline, name="timeline"),
-] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
+]
diff --git a/pydis_site/apps/home/views/__init__.py b/pydis_site/apps/home/views/__init__.py
index 36b88b1b..28cc4d65 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, timeline
-__all__ = ["AccountDeleteView", "AccountSettingsView", "HomeView", "timeline"]
+__all__ = ["HomeView", "timeline"]
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/redirect/__init__.py b/pydis_site/apps/redirect/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/pydis_site/apps/redirect/__init__.py
diff --git a/pydis_site/apps/redirect/apps.py b/pydis_site/apps/redirect/apps.py
new file mode 100644
index 00000000..9b70d169
--- /dev/null
+++ b/pydis_site/apps/redirect/apps.py
@@ -0,0 +1,7 @@
+from django.apps import AppConfig
+
+
+class RedirectConfig(AppConfig):
+ """AppConfig instance for Redirect app."""
+
+ name = 'redirect'
diff --git a/pydis_site/apps/redirect/migrations/__init__.py b/pydis_site/apps/redirect/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/pydis_site/apps/redirect/migrations/__init__.py
diff --git a/pydis_site/apps/redirect/redirects.yaml b/pydis_site/apps/redirect/redirects.yaml
new file mode 100644
index 00000000..ce789b61
--- /dev/null
+++ b/pydis_site/apps/redirect/redirects.yaml
@@ -0,0 +1,194 @@
+# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+# The redirects here are for dewikification backwards compatibility
+# and SHOULD NOT be used for adding new redirects for convenience.
+#
+# Convenience redirects should be added using our cloudflare worker
+# at https://github.com/python-discord/workers/tree/main/short-urls
+# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+# Root pages
+roles_redirect:
+ original_path: pages/roles/
+ redirect_route: "content:page_category"
+ redirect_arguments: ["server-info/roles"]
+
+roles_expectations_redirect:
+ original_path: pages/roles/staff-role-expectations/
+ redirect_route: "content:page_category"
+ redirect_arguments: ["server-info/staff-role-expectations"]
+
+contributing_redirect:
+ original_path: pages/contributing/
+ redirect_route: "content:page_category"
+ redirect_arguments: ["guides/pydis-guides/contributing"]
+
+# Guides
+guides_redirect:
+ original_path: pages/resources/guides/
+ redirect_route: "content:page_category"
+ redirect_arguments: ["guides"]
+
+# - Python guides
+discord_py_redirect:
+ original_path: pages/resources/guides/discordpy/
+ redirect_route: "content:page_category"
+ redirect_arguments: ["guides/python-guides/discordpy"]
+
+mutability_redirect:
+ original_path: pages/resources/guides/core-concepts/mutability/
+ redirect_route: "content:page_category"
+ redirect_arguments: ["guides/python-guides/mutability"]
+
+function_params_redirect:
+ original_path: pages/resources/guides/core-concepts/parameters-and-arguments/
+ redirect_route: "content:page_category"
+ redirect_arguments: ["guides/python-guides/parameters-and-arguments"]
+
+# - Pydis guides
+help_channel_redirect:
+ original_path: pages/resources/guides/help-channels/
+ redirect_route: "content:page_category"
+ redirect_arguments: ["guides/pydis-guides/help-channel-guide"]
+
+good_questions_redirect:
+ original_path: pages/resources/guides/asking-good-questions/
+ redirect_route: "content:page_category"
+ redirect_arguments: ["guides/pydis-guides/asking-good-questions"]
+
+helping_others_redirect:
+ original_path: pages/resources/guides/helping-others/
+ redirect_route: "content:page_category"
+ redirect_arguments: ["guides/pydis-guides/helping-others"]
+
+code_review_redirect:
+ original_path: pages/resources/guides/code-reviews-primer/
+ redirect_route: "content:page_category"
+ redirect_arguments: ["guides/pydis-guides/code-reviews-primer"]
+
+off-topic_redirect:
+ original_path: pages/resources/guides/off-topic-etiquette/
+ redirect_route: "content:page_category"
+ redirect_arguments: ["guides/pydis-guides/off-topic-etiquette"]
+
+# Resources
+resources_index_redirect:
+ original_path: pages/resources/
+ redirect_route: "resources:index"
+
+resources_resources_redirect:
+ original_path: pages/resources/<str:category>/
+ redirect_route: "resources:resources"
+
+# Events
+events_index_redirect:
+ original_path: pages/events/
+ redirect_route: "events:index"
+
+events_code_jams_index_redirect:
+ original_path: pages/code-jams/
+ redirect_route: "events:page"
+ redirect_arguments: ["code-jams"]
+
+events_code_jams_one_redirect:
+ original_path: pages/code-jams/code-jam-1-snakes-bot/
+ redirect_route: "events:page"
+ redirect_arguments: ["code-jams/1"]
+
+events_code_jams_two_redirect:
+ original_path: pages/code-jams/code-jam-2/
+ redirect_route: "events:page"
+ redirect_arguments: ["code-jams/2"]
+
+events_code_jams_three_redirect:
+ original_path: pages/code-jams/code-jam-3/
+ redirect_route: "events:page"
+ redirect_arguments: ["code-jams/3"]
+
+events_code_jams_four_redirect:
+ original_path: pages/code-jams/code-jam-4/
+ redirect_route: "events:page"
+ redirect_arguments: ["code-jams/4"]
+
+events_code_jams_five_redirect:
+ original_path: pages/code-jams/code-jam-5/
+ redirect_route: "events:page"
+ redirect_arguments: ["code-jams/5"]
+
+events_code_jams_six_redirect:
+ original_path: pages/code-jams/code-jam-6/
+ redirect_route: "events:page"
+ redirect_arguments: ["code-jams/6"]
+
+events_code_jams_six_rules_redirect:
+ original_path: pages/code-jams/code-jam-6/rules/
+ redirect_route: "events:page"
+ redirect_arguments: ["code-jams/6/rules"]
+
+events_code_jams_seven_redirect:
+ original_path: pages/code-jams/code-jam-7/
+ redirect_route: "events:page"
+ redirect_arguments: ["code-jams/7"]
+
+events_code_jams_seven_rules_redirect:
+ original_path: pages/code-jams/code-jam-7/rules/
+ redirect_route: "events:page"
+ redirect_arguments: ["code-jams/7/rules"]
+
+events_code_jams_how_to_use_git_redirect:
+ original_path: pages/code-jams/using-git/
+ redirect_route: "events:page"
+ redirect_arguments: ["code-jams/using-git"]
+
+events_code_jams_judging_redirect:
+ original_path: pages/code-jams/judging/
+ redirect_route: "events:page"
+ redirect_arguments: ["code-jams/judging"]
+
+events_code_jams_pull_request_redirect:
+ original_path: pages/code-jams/pull-request/
+ redirect_route: "events:page"
+ redirect_arguments: ["code-jams/pull-request"]
+
+events_game_jams_twenty_twenty_index_redirect:
+ original_path: pages/events/game-jam-2020/
+ redirect_route: "events:page"
+ redirect_arguments: ["game-jams/2020"]
+
+events_game_jams_twenty_twenty_judging_redirect:
+ original_path: pages/events/game-jam-2020/judging/
+ redirect_route: "events:page"
+ redirect_arguments: ["game-jams/2020/judging"]
+
+events_game_jams_twenty_twenty_project_setup_redirect:
+ original_path: pages/events/game-jam-2020/project-setup/
+ redirect_route: "events:page"
+ redirect_arguments: ["game-jams/2020/project-setup"]
+
+events_game_jams_twenty_twenty_rules_redirect:
+ original_path: pages/events/game-jam-2020/rules/
+ redirect_route: "events:page"
+ redirect_arguments: ["game-jams/2020/rules"]
+
+events_game_jams_twenty_twenty_technical_requirements_redirect:
+ original_path: pages/events/game-jam-2020/technical-requirements
+ redirect_route: "events:page"
+ redirect_arguments: ["game-jams/2020/technical-requirements"]
+
+# This are overrides for the contributing prefix redirect
+security_notice_redirect:
+ original_path: pages/contributing/security-notice/
+ redirect_route: "content:page_category"
+ redirect_arguments: ["security-notice"]
+
+sir-lancebot_env_var_redirect:
+ original_path: pages/contributing/sir-lancebot/sir-lancebot-env-var-reference/
+ redirect_route: "content:page_category"
+ redirect_arguments: ["guides/pydis-guides/contributing/sir-lancebot/env-var-reference"]
+
+# Prefix redirects
+# Prefix redirects must be last in each group.
+guides_pydis_guides_contributing_prefix_redirect:
+ original_path: pages/contributing/<path:path>/ # path:path will be joined together with static arguments.
+ redirect_route: "content:page_category"
+ redirect_arguments: ["guides/pydis-guides/contributing/"] # It is important to put / at end in prefix redirect!
+ prefix_redirect: true
diff --git a/pydis_site/apps/redirect/tests.py b/pydis_site/apps/redirect/tests.py
new file mode 100644
index 00000000..fce2642f
--- /dev/null
+++ b/pydis_site/apps/redirect/tests.py
@@ -0,0 +1,61 @@
+import yaml
+from django.conf import settings
+from django.test import TestCase
+from django.urls import reverse
+
+TESTING_ARGUMENTS = {
+ "resources_resources_redirect": ("reading",),
+ "guides_pydis_guides_contributing_prefix_redirect": ("sir-lancebot/env-var-reference",),
+}
+
+
+class RedirectTests(TestCase):
+ """Survival tests for redirects."""
+
+ def test_redirects(self) -> None:
+ """
+ Should redirect to given route based on redirect rules.
+
+ Makes sure that every redirect:
+ 1. Redirects only once.
+ 2. Redirects to right URL.
+ 3. Resulting page status code is 200.
+ """
+ for name, data in yaml.safe_load(settings.REDIRECTIONS_PATH.read_text()).items():
+ with self.subTest(
+ original_path=data["original_path"],
+ redirect_route=data["redirect_route"],
+ name=name,
+ redirect_arguments=tuple(data.get("redirect_arguments", ())),
+ args=TESTING_ARGUMENTS.get(name, ())
+ ):
+ resp = self.client.get(
+ reverse(
+ f"home:redirect:{name}",
+ args=TESTING_ARGUMENTS.get(name, ())
+ ),
+ follow=True
+ )
+
+ if data.get("prefix_redirect", False):
+ expected_args = (
+ "".join(
+ tuple(data.get("redirect_arguments", ()))
+ + TESTING_ARGUMENTS.get(name, ())
+ ),
+ )
+ else:
+ expected_args = (
+ TESTING_ARGUMENTS.get(name, ()) + tuple(data.get("redirect_arguments", ()))
+ )
+
+ self.assertEqual(1, len(resp.redirect_chain))
+ self.assertRedirects(
+ resp,
+ reverse(
+ f"home:{data['redirect_route']}",
+ args=expected_args
+ ),
+ status_code=302
+ )
+ self.assertEqual(resp.status_code, 200)
diff --git a/pydis_site/apps/redirect/urls.py b/pydis_site/apps/redirect/urls.py
new file mode 100644
index 00000000..6187af17
--- /dev/null
+++ b/pydis_site/apps/redirect/urls.py
@@ -0,0 +1,19 @@
+import yaml
+from django.conf import settings
+from django.urls import path
+
+from pydis_site.apps.redirect.views import CustomRedirectView
+
+app_name = "redirect"
+urlpatterns = [
+ path(
+ data["original_path"],
+ CustomRedirectView.as_view(
+ pattern_name=data["redirect_route"],
+ static_args=tuple(data.get("redirect_arguments", ())),
+ prefix_redirect=data.get("prefix_redirect", False)
+ ),
+ name=name
+ )
+ for name, data in yaml.safe_load(settings.REDIRECTIONS_PATH.read_text()).items()
+]
diff --git a/pydis_site/apps/redirect/views.py b/pydis_site/apps/redirect/views.py
new file mode 100644
index 00000000..9dc9881a
--- /dev/null
+++ b/pydis_site/apps/redirect/views.py
@@ -0,0 +1,26 @@
+import typing as t
+
+from django.views.generic import RedirectView
+
+
+class CustomRedirectView(RedirectView):
+ """Extended RedirectView for manual route args."""
+
+ # We want temporary redirects for the time being, after this is running on prod and
+ # stable we can enable permanent redirects.
+ permanent = False
+ static_args = ()
+ prefix_redirect = False
+
+ @classmethod
+ def as_view(cls, **initkwargs):
+ """Overwrites original as_view to add static args."""
+ return super().as_view(**initkwargs)
+
+ def get_redirect_url(self, *args, **kwargs) -> t.Optional[str]:
+ """Extends default behaviour to use static args."""
+ args = self.static_args + args + tuple(kwargs.values())
+ if self.prefix_redirect:
+ args = ("".join(args),)
+
+ return super().get_redirect_url(*args)
diff --git a/pydis_site/apps/resources/__init__.py b/pydis_site/apps/resources/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ 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/apps/resources/migrations/__init__.py b/pydis_site/apps/resources/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/pydis_site/apps/resources/migrations/__init__.py
diff --git a/pydis_site/apps/home/resources/communities/_category_info.yaml b/pydis_site/apps/resources/resources/communities/_category_info.yaml
index eccb8b80..b9cb6533 100644
--- a/pydis_site/apps/home/resources/communities/_category_info.yaml
+++ b/pydis_site/apps/resources/resources/communities/_category_info.yaml
@@ -1,2 +1,2 @@
-description: Partnered communities that share part of our mission
+description: Partnered communities that share part of our mission.
name: Communities
diff --git a/pydis_site/apps/resources/resources/communities/adafruit.yaml b/pydis_site/apps/resources/resources/communities/adafruit.yaml
new file mode 100644
index 00000000..e5c81a6c
--- /dev/null
+++ b/pydis_site/apps/resources/resources/communities/adafruit.yaml
@@ -0,0 +1,15 @@
+description: Adafruit is an open-source electronics manufacturer
+ that makes all the components you need to start your own Python-powered hardware projects.
+ Their official community host regular show-and-tells,
+ provide help with your projects,
+ and the Adafruit devs do all the CircuitPython Development right out in the open.
+title_image: https://www.mouser.com/images/suppliers/logos/adafruit.png
+title_url: https://discord.gg/adafruit
+position: 4
+urls:
+- icon: branding/discord
+ url: https://discord.gg/adafruit
+ color: blurple
+- icon: regular/link
+ url: https://adafruit.com/
+ color: teal
diff --git a/pydis_site/apps/resources/resources/communities/awesome_programming_discord.yaml b/pydis_site/apps/resources/resources/communities/awesome_programming_discord.yaml
new file mode 100644
index 00000000..335ac507
--- /dev/null
+++ b/pydis_site/apps/resources/resources/communities/awesome_programming_discord.yaml
@@ -0,0 +1,9 @@
+description: We have listed our favourite communities,
+ but there are many more excellent communities out there!
+ An awesome list collating the best programming related Discord servers is available on GitHub
+ and has all sorts of topics from blockchain to virtual reality!
+title_icon: branding/github
+title_icon_color: black
+title_url: https://github.com/mhxion/awesome-programming-discord
+name: awesome-programming-discord
+position: 10
diff --git a/pydis_site/apps/resources/resources/communities/kivy.yaml b/pydis_site/apps/resources/resources/communities/kivy.yaml
new file mode 100644
index 00000000..601d7dba
--- /dev/null
+++ b/pydis_site/apps/resources/resources/communities/kivy.yaml
@@ -0,0 +1,18 @@
+description: The Kivy project, through the Kivy framework and its sister projects,
+ aims to provide all the tools to create desktop and mobile applications in Python.
+ Allowing rapid development of multitouch applications with custom and exciting user interfaces.
+icon_image: https://raw.githubusercontent.com/kivy/kivy-website/master/logos/kivy-logo-black-256.png
+icon_size: 50
+title_image: https://i.imgur.com/EVP3jZR.png
+title_url: https://discord.gg/djPtTRJ
+position: 5
+urls:
+ - icon: branding/discord
+ url: https://discord.gg/djPtTRJ
+ color: blurple
+ - icon: regular/link
+ url: https://kivy.org/
+ color: teal
+ - icon: branding/github
+ url: https://github.com/kivy
+ color: black
diff --git a/pydis_site/apps/resources/resources/communities/microsoft.yaml b/pydis_site/apps/resources/resources/communities/microsoft.yaml
new file mode 100644
index 00000000..b36c3a85
--- /dev/null
+++ b/pydis_site/apps/resources/resources/communities/microsoft.yaml
@@ -0,0 +1,12 @@
+description: Microsoft Python is a Discord server for discussing all things relating to using Python with Microsoft products,
+ they have channels for Azure, VS Code, IoT, Data Science and much more!
+title_image: https://1000logos.net/wp-content/uploads/2017/04/Microsoft-Logo.png
+title_url: https://discord.gg/b8YJQPx
+position: 1
+urls:
+ - icon: branding/discord
+ url: https://discord.gg/b8YJQPx
+ color: blurple
+ - icon: regular/link
+ url: https://www.microsoft.com/en-us/boards/pycon2020.aspx
+ color: teal
diff --git a/pydis_site/apps/resources/resources/communities/pallets.yaml b/pydis_site/apps/resources/resources/communities/pallets.yaml
new file mode 100644
index 00000000..239b1491
--- /dev/null
+++ b/pydis_site/apps/resources/resources/communities/pallets.yaml
@@ -0,0 +1,13 @@
+description: The Pallets Projects develop Python libraries such as the Flask web framework,
+ the Jinja templating library, and the Click command line toolkit. Join to discuss
+ and get help from the Pallets community.
+title_image: https://i.imgur.com/sV9Ypdf.png
+title_url: https://discord.gg/t6rrQZH
+position: 6
+urls:
+ - icon: branding/discord
+ url: https://discord.gg/t6rrQZH
+ color: blurple
+ - icon: regular/link
+ url: https://www.palletsprojects.com/
+ color: teal
diff --git a/pydis_site/apps/resources/resources/communities/panda3d.yaml b/pydis_site/apps/resources/resources/communities/panda3d.yaml
new file mode 100644
index 00000000..4235793d
--- /dev/null
+++ b/pydis_site/apps/resources/resources/communities/panda3d.yaml
@@ -0,0 +1,12 @@
+description: Panda3D is a Python-focused 3-D framework for rapid development of games,
+ visualizations, and simulations, written in C++ with an emphasis on performance and flexibility.
+title_image: http://www.panda3d.org/wp-content/uploads/2019/01/panda3d_logo.png
+title_url: https://discord.gg/9XsucTT
+position: 9
+urls:
+ - icon: branding/discord
+ url: https://discord.gg/9XsucTT
+ color: blurple
+ - icon: regular/link
+ url: https://www.panda3d.org/
+ color: teal
diff --git a/pydis_site/apps/resources/resources/communities/people_postgres_data.yaml b/pydis_site/apps/resources/resources/communities/people_postgres_data.yaml
new file mode 100644
index 00000000..1c17d343
--- /dev/null
+++ b/pydis_site/apps/resources/resources/communities/people_postgres_data.yaml
@@ -0,0 +1,18 @@
+description: People, Postgres, Data specializes in building users of Postgres
+ and related ecosystem including but not limited to technologies such as RDS Postgres,
+ Aurora for Postgres, Google Postgres, PostgreSQL.Org Postgres, Greenplum, Timescale and ZomboDB.
+ They take a holistic approach to their community inviting not only technical topics but Professional Development
+ and Life in general including movies, games, books and travel.
+title_image: https://media.discordapp.net/attachments/748954447857844318/750519488268730377/people_postgres_data.png
+title_url: https://discord.gg/Ujw8m8v
+position: 2
+urls:
+ - icon: branding/discord
+ url: https://discord.gg/Ujw8m8v
+ color: bluple
+ - icon: regular/link
+ url: https://postgresconf.org/
+ color: teal
+ - icon: branding/reddit
+ url: https://reddit.com/r/postgresql
+ color: orangered
diff --git a/pydis_site/apps/resources/resources/communities/pyglet.yaml b/pydis_site/apps/resources/resources/communities/pyglet.yaml
new file mode 100644
index 00000000..784f514e
--- /dev/null
+++ b/pydis_site/apps/resources/resources/communities/pyglet.yaml
@@ -0,0 +1,15 @@
+description: Pyglet is a powerful,
+ yet easy to use Python library for developing games and other visually-rich applications on Windows,
+ Mac OS X and Linux. It supports windowing, user interface event handling, Joysticks, OpenGL graphics,
+ loading images and videos, and playing sounds and music. All of this with a friendly Pythonic API,
+ that's simple to learn and doesn't get in your way.
+title_image: https://i.imgur.com/LfQwXUe.png
+title_url: https://discord.gg/QXyegWe
+position: 8
+urls:
+ - icon: branding/discord
+ url: https://discord.gg/QXyegWe
+ color: blurple
+ - icon: regular/link
+ url: http://pyglet.org/
+ color: teal
diff --git a/pydis_site/apps/resources/resources/communities/real_python.yaml b/pydis_site/apps/resources/resources/communities/real_python.yaml
new file mode 100644
index 00000000..1fc74d93
--- /dev/null
+++ b/pydis_site/apps/resources/resources/communities/real_python.yaml
@@ -0,0 +1,12 @@
+description: Dan Bader's treasure trove of quizzes, tutorials and interactive content for learning Python.
+ An absolute goldmine.
+title_image: https://i.imgur.com/WDqhZ36.png
+title_url: https://realpython.com/
+position: 3
+urls:
+ - icon: regular/link
+ url: https://realpython.com/
+ color: teal
+ - icon: branding/youtube
+ url: https://www.youtube.com/channel/UCI0vQvr9aFn27yR6Ej6n5UA
+ color: youtube-red
diff --git a/pydis_site/apps/resources/resources/communities/rlbot.yaml b/pydis_site/apps/resources/resources/communities/rlbot.yaml
new file mode 100644
index 00000000..d12c1dec
--- /dev/null
+++ b/pydis_site/apps/resources/resources/communities/rlbot.yaml
@@ -0,0 +1,13 @@
+description: RLBot is a community of programmers making awesome Rocket League bots.
+ They've created a framework that you can use to write bots in a number of languages (including Python),
+ and they host regular tournaments where botmakers can pit their creations against each other.
+title_image: https://i.imgur.com/S8L1muZ.png
+title_url: https://discord.gg/4JJdJKb
+position: 7
+urls:
+ - icon: branding/discord
+ url: https://discord.gg/4JJdJKb
+ color: blurple
+ - icon: regular/link
+ url: https://www.rlbot.org/
+ color: teal
diff --git a/pydis_site/apps/resources/resources/communities/subreddit.yaml b/pydis_site/apps/resources/resources/communities/subreddit.yaml
new file mode 100644
index 00000000..d3ddb15a
--- /dev/null
+++ b/pydis_site/apps/resources/resources/communities/subreddit.yaml
@@ -0,0 +1,6 @@
+description: News about the Python programming language, and language-related discussion.
+name: r/Python
+title_icon: branding/reddit
+title_icon_color: orangered
+title_url: https://www.reddit.com/r/Python/
+position: 0
diff --git a/pydis_site/apps/resources/resources/courses/_category_info.yaml b/pydis_site/apps/resources/resources/courses/_category_info.yaml
new file mode 100644
index 00000000..948b48de
--- /dev/null
+++ b/pydis_site/apps/resources/resources/courses/_category_info.yaml
@@ -0,0 +1,4 @@
+description: Listing of best Python courses.
+name: Courses
+default_icon: regular/graduation-cap
+default_icon_color: black
diff --git a/pydis_site/apps/resources/resources/courses/automate_the_boring_stuff_with_python.yaml b/pydis_site/apps/resources/resources/courses/automate_the_boring_stuff_with_python.yaml
new file mode 100644
index 00000000..66034ea2
--- /dev/null
+++ b/pydis_site/apps/resources/resources/courses/automate_the_boring_stuff_with_python.yaml
@@ -0,0 +1,5 @@
+description: The interactive course version of Al Sweigart's excellent book for beginners, taught by the author himself.
+ This link has a discounted version of the course which will always cost 10 dollars. Thanks, Al!
+name: Automate the Boring Stuff with Python
+title_url: https://www.udemy.com/automate/?couponCode=FOR_LIKE_10_BUCKS
+position: 3
diff --git a/pydis_site/apps/resources/resources/courses/mit_introduction_to_computer_science_and_programming.yaml b/pydis_site/apps/resources/resources/courses/mit_introduction_to_computer_science_and_programming.yaml
new file mode 100644
index 00000000..5560b2cb
--- /dev/null
+++ b/pydis_site/apps/resources/resources/courses/mit_introduction_to_computer_science_and_programming.yaml
@@ -0,0 +1,6 @@
+description: This MITx offering teaches computer science with Python.
+ It covers computational thinking, algorithms, data structures
+ and the Python programming language itself.
+name: 'MIT: Introduction to Computer Science and Programming'
+title_url: https://www.edx.org/course/introduction-computer-science-mitx-6-00-1x-11
+position: 1
diff --git a/pydis_site/apps/resources/resources/courses/practical_python_programming.yaml b/pydis_site/apps/resources/resources/courses/practical_python_programming.yaml
new file mode 100644
index 00000000..b801ca8c
--- /dev/null
+++ b/pydis_site/apps/resources/resources/courses/practical_python_programming.yaml
@@ -0,0 +1,9 @@
+description: Created and taught by <a href="https://dabeaz.com/">David Beazley</a>,
+ this course is a conversion of his instructor-led Python training course used for corporate training
+ and professional development. It has been in continual development since 2007
+ and battle tested in real-world classrooms. Usually, it’s taught in-person over the span of three
+ or four days–requiring approximately 25-35 hours of intense work.
+ This includes the completion of approximately 130 hands-on coding exercises.
+name: Practical Python Programming
+title_url: https://dabeaz-course.github.io/practical-python/
+position: 4
diff --git a/pydis_site/apps/resources/resources/courses/university_of_michigan.yaml b/pydis_site/apps/resources/resources/courses/university_of_michigan.yaml
new file mode 100644
index 00000000..3efe7640
--- /dev/null
+++ b/pydis_site/apps/resources/resources/courses/university_of_michigan.yaml
@@ -0,0 +1,5 @@
+description: A 5-part specialization course that teaches Python from scratch.
+ The course has no pre-requisites and avoids all but the simplest mathematics.
+name: 'University of Michigan: Programming for Everybody'
+title_url: https://www.coursera.org/learn/python
+position: 2
diff --git a/pydis_site/apps/resources/resources/courses/university_of_toronto.yaml b/pydis_site/apps/resources/resources/courses/university_of_toronto.yaml
new file mode 100644
index 00000000..0a7839de
--- /dev/null
+++ b/pydis_site/apps/resources/resources/courses/university_of_toronto.yaml
@@ -0,0 +1,11 @@
+description: A 2-part course that teaches Python. Primarily intended for high school students
+ and first-year university students who want to learn programming.
+name: 'University of Toronto: Learn to Program'
+position: 0
+urls:
+ - icon: regular/graduation-cap
+ url: https://www.coursera.org/learn/learn-to-program
+ color: orangered
+ - icon: regular/graduation-cap
+ url: https://www.coursera.org/learn/program-code
+ color: youtube-red
diff --git a/pydis_site/apps/resources/resources/interactive/_category_info.yaml b/pydis_site/apps/resources/resources/interactive/_category_info.yaml
new file mode 100644
index 00000000..7e8f34d9
--- /dev/null
+++ b/pydis_site/apps/resources/resources/interactive/_category_info.yaml
@@ -0,0 +1,4 @@
+description: Learn Python with interactive courses, games, and programming challenges.
+name: Interactive
+default_icon: branding/python
+default_icon_color: black
diff --git a/pydis_site/apps/home/resources/interactive_learning_tools/code_combat.yaml b/pydis_site/apps/resources/resources/interactive/code_combat.yaml
index 39c25f0d..30f20c28 100644
--- a/pydis_site/apps/home/resources/interactive_learning_tools/code_combat.yaml
+++ b/pydis_site/apps/resources/resources/interactive/code_combat.yaml
@@ -1,13 +1,11 @@
description: Learn Python while gaming - an open-source project with thousands of
contributors, which teaches you Python through a deep, top-down RPG.
name: Code Combat
-payment: optional
-payment_description: A wealth of free content is available, but you can also pay for
- more
+position: 0
urls:
- icon: regular/link
- title: Website
url: https://codecombat.com/
+ color: teal
- icon: branding/github
- title: GitHub
url: https://github.com/codecombat/codecombat
+ color: black
diff --git a/pydis_site/apps/resources/resources/interactive/edublocks.yaml b/pydis_site/apps/resources/resources/interactive/edublocks.yaml
new file mode 100644
index 00000000..7c6ca02b
--- /dev/null
+++ b/pydis_site/apps/resources/resources/interactive/edublocks.yaml
@@ -0,0 +1,10 @@
+description: EduBlocks provides a simple drag and drop interface to help beginners get to grips
+ with the key concepts of Python. There is built-in support for modules such as random,
+ turtle, processing and pygal to play around with,
+ and it even allows you to export the Python code you have written in the graphical editor.
+ EduBlocks also has integration with BBC micro:bit,
+ Raspberry Pi and CircuitPython allowing you to write code for these devices graphically
+ and export the code to run on actual devices.
+name: EduBlocks
+title_url: https://edublocks.org/
+position: 5
diff --git a/pydis_site/apps/home/resources/interactive_learning_tools/exercism.yaml b/pydis_site/apps/resources/resources/interactive/exercism.yaml
index 3adb4138..68b458d0 100644
--- a/pydis_site/apps/home/resources/interactive_learning_tools/exercism.yaml
+++ b/pydis_site/apps/resources/resources/interactive/exercism.yaml
@@ -3,12 +3,11 @@ description: Level up your programming skills with more than 2600 exercises acro
where you can get your code reviewed for each solution you submit. The mentors will
give you insightful advice to make you a better programmer.
name: exercism.io
-payment: free
-payment_description: null
+position: 1
urls:
- icon: regular/link
- title: Website
url: https://exercism.io/
+ color: teal
- icon: branding/github
- title: GitHub
url: https://github.com/exercism/python
+ color: black
diff --git a/pydis_site/apps/resources/resources/interactive/jetbrains_academy.yaml b/pydis_site/apps/resources/resources/interactive/jetbrains_academy.yaml
new file mode 100644
index 00000000..937831fa
--- /dev/null
+++ b/pydis_site/apps/resources/resources/interactive/jetbrains_academy.yaml
@@ -0,0 +1,8 @@
+description: Learn Python with a wide range of high quality, project-based lessons.
+ Keep track of your progress as you cover all the basic concepts a Python programmer needs to know,
+ as well as touching on more advanced areas such as web development with Django,
+ natural language processing with NLTK, and data science with NumPy, pandas, and scikit-learn!
+ It requires a paid subscription, but a free trial is available.
+name: JetBrains Academy
+title_url: https://www.jetbrains.com/academy/
+position: 6
diff --git a/pydis_site/apps/home/resources/interactive_learning_tools/python_morsels.yaml b/pydis_site/apps/resources/resources/interactive/python_morsels.yaml
index f883f8b7..879500eb 100644
--- a/pydis_site/apps/home/resources/interactive_learning_tools/python_morsels.yaml
+++ b/pydis_site/apps/resources/resources/interactive/python_morsels.yaml
@@ -6,10 +6,5 @@ description: 'Learn to write more idiomatic Python code with deliberate practice
to the exercise with explanations of each one. Each exercise will include automated
tests and some may include bonuses for a little more of a challenge!'
name: Python Morsels
-payment: paid
-payment_description: Paid service with monthly and annual plans. A 4 week free trial
- is available without needing to enter payment information.
-urls:
-- icon: regular/link
- title: Website
- url: https://www.pythonmorsels.com/
+title_url: https://www.pythonmorsels.com/
+position: 3
diff --git a/pydis_site/apps/resources/resources/interactive/python_tutor.yaml b/pydis_site/apps/resources/resources/interactive/python_tutor.yaml
new file mode 100644
index 00000000..64b50d09
--- /dev/null
+++ b/pydis_site/apps/resources/resources/interactive/python_tutor.yaml
@@ -0,0 +1,4 @@
+description: Write Python code in your web browser, and see it visualized step by step.
+name: Python Tutor
+title_url: https://www.pythontutor.com/
+position: 2
diff --git a/pydis_site/apps/resources/resources/interactive/sololearn.yaml b/pydis_site/apps/resources/resources/interactive/sololearn.yaml
new file mode 100644
index 00000000..51dceb2a
--- /dev/null
+++ b/pydis_site/apps/resources/resources/interactive/sololearn.yaml
@@ -0,0 +1,7 @@
+description: SoloLearn's Python 3 course serves as a simple and convenient introduction to Python.
+ Containing exercises and quizzes in modules to cover introductory subjects of the language,
+ you can pick it up and put it down between your busier aspects of life thanks to both PC
+ and mobile apps being available to use.
+name: SoloLearn
+title_url: https://www.sololearn.com/Course/Python/
+position: 4
diff --git a/pydis_site/apps/resources/resources/podcasts/_category_info.yaml b/pydis_site/apps/resources/resources/podcasts/_category_info.yaml
new file mode 100644
index 00000000..1d2d3ba5
--- /dev/null
+++ b/pydis_site/apps/resources/resources/podcasts/_category_info.yaml
@@ -0,0 +1,4 @@
+description: Notable podcasts about the Python ecosystem.
+name: Podcasts
+default_icon: regular/microphone-alt
+default_icon_color: black
diff --git a/pydis_site/apps/home/resources/podcasts/podcast_dunder_init.yaml b/pydis_site/apps/resources/resources/podcasts/podcast_dunder_init.yaml
index 8f0cac8b..efe1601f 100644
--- a/pydis_site/apps/home/resources/podcasts/podcast_dunder_init.yaml
+++ b/pydis_site/apps/resources/resources/podcasts/podcast_dunder_init.yaml
@@ -1,9 +1,5 @@
description: The podcast about Python and the people who make it great. Weekly long-form
interviews with the creators of notable Python packages.
name: Podcast.__init__
-payment: free
-payment_description: null
-urls:
-- icon: regular/link
- title: Website
- url: https://www.podcastinit.com/
+title_url: https://www.podcastinit.com/
+position: 2
diff --git a/pydis_site/apps/home/resources/podcasts/python_bytes.yaml b/pydis_site/apps/resources/resources/podcasts/python_bytes.yaml
index a3368d23..4f817f26 100644
--- a/pydis_site/apps/home/resources/podcasts/python_bytes.yaml
+++ b/pydis_site/apps/resources/resources/podcasts/python_bytes.yaml
@@ -1,9 +1,5 @@
description: A byte-sized podcast where Michael Kennedy and Brian Okken work through
this week's notable Python headlines.
name: Python Bytes
-payment: free
-payment_description: null
-urls:
-- icon: regular/link
- title: Website
- url: https://pythonbytes.fm/
+title_url: https://pythonbytes.fm/
+position: 1
diff --git a/pydis_site/apps/home/resources/podcasts/talk_python_to_me.yaml b/pydis_site/apps/resources/resources/podcasts/talk_python_to_me.yaml
index 5ed101c4..5ce21fd7 100644
--- a/pydis_site/apps/home/resources/podcasts/talk_python_to_me.yaml
+++ b/pydis_site/apps/resources/resources/podcasts/talk_python_to_me.yaml
@@ -1,9 +1,5 @@
description: The essential weekly Python podcast. Michael Kennedy and a prominent
name within the Python community dive into a topic that relates to their experience.
name: Talk Python To Me
-payment: free
-payment_description: null
-urls:
-- icon: regular/link
- title: Website
- url: https://talkpython.fm/
+title_url: https://talkpython.fm/
+position: 0
diff --git a/pydis_site/apps/resources/resources/podcasts/test_and_code.yaml b/pydis_site/apps/resources/resources/podcasts/test_and_code.yaml
new file mode 100644
index 00000000..d5751577
--- /dev/null
+++ b/pydis_site/apps/resources/resources/podcasts/test_and_code.yaml
@@ -0,0 +1,5 @@
+description: Brian Okken's weekly podcast on testing. Usually deals with Python,
+ but also covers many language-agnostic topics from the testing and DevOps world.
+name: Test & Code
+title_url: https://testandcode.com/
+position: 3
diff --git a/pydis_site/apps/resources/resources/podcasts/the_real_python_podcast.yaml b/pydis_site/apps/resources/resources/podcasts/the_real_python_podcast.yaml
new file mode 100644
index 00000000..dea894ea
--- /dev/null
+++ b/pydis_site/apps/resources/resources/podcasts/the_real_python_podcast.yaml
@@ -0,0 +1,7 @@
+description: A weekly Python podcast hosted by Christopher Bailey with interviews,
+ coding tips, and conversation with guests from the Python community.
+ The show covers a wide range of topics including Python programming best practices,
+ career tips, and related software development topics.
+name: The Real Python Podcast
+title_url: https://realpython.com/podcasts/rpp/
+position: 4
diff --git a/pydis_site/apps/resources/resources/reading/_category_info.yaml b/pydis_site/apps/resources/resources/reading/_category_info.yaml
new file mode 100644
index 00000000..64b87e47
--- /dev/null
+++ b/pydis_site/apps/resources/resources/reading/_category_info.yaml
@@ -0,0 +1,2 @@
+description: Books and tutorials related to Python and popular third-party libraries and frameworks.
+name: Reading
diff --git a/pydis_site/apps/home/resources/books/_category_info.yaml b/pydis_site/apps/resources/resources/reading/books/_category_info.yaml
index e3b89ad3..ae092a20 100644
--- a/pydis_site/apps/home/resources/books/_category_info.yaml
+++ b/pydis_site/apps/resources/resources/reading/books/_category_info.yaml
@@ -1,2 +1,5 @@
-description: The best books for learning Python or Python Frameworks
+description: The best books for learning Python or Python Frameworks.
name: Books
+default_icon: branding/python
+default_icon_color: black
+position: 0
diff --git a/pydis_site/apps/home/resources/books/automate_the_boring_stuff.yaml b/pydis_site/apps/resources/resources/reading/books/automate_the_boring_stuff.yaml
index 3a9045a5..3812029c 100644
--- a/pydis_site/apps/home/resources/books/automate_the_boring_stuff.yaml
+++ b/pydis_site/apps/resources/resources/reading/books/automate_the_boring_stuff.yaml
@@ -4,13 +4,11 @@ description: One of the best books out there for Python beginners. This book wil
the web, manipulating files and automating keyboard and mouse input. Ideal for an
office worker who wants to make himself more useful.
name: Automate the Boring Stuff with Python
-payment: optional
-payment_description: A free e-book is available on the website, but you can buy it
- on Amazon if you want to support the author.
+position: 2
urls:
-- icon: regular/link
- title: E-book
+- icon: regular/book
url: https://automatetheboringstuff.com/
+ color: black
- icon: branding/amazon
- title: Amazon
url: https://www.amazon.com/Automate-Boring-Stuff-Python-Programming/dp/1593275994/
+ color: amazon-orange
diff --git a/pydis_site/apps/resources/resources/reading/books/byte_of_python.yaml b/pydis_site/apps/resources/resources/reading/books/byte_of_python.yaml
new file mode 100644
index 00000000..1f9642ad
--- /dev/null
+++ b/pydis_site/apps/resources/resources/reading/books/byte_of_python.yaml
@@ -0,0 +1,15 @@
+description: A free book on programming using the Python language.
+ It serves as a tutorial or guide to the Python language for a beginner audience.
+ If all you know about computers is how to save text files, then this is the book for you.
+name: A Byte of Python
+position: 1
+urls:
+- icon: regular/link
+ url: https://python.swaroopch.com/
+ color: teal
+- icon: regular/book
+ url: http://www.lulu.com/shop/swaroop-c-h/a-byte-of-python/paperback/product-21142968.html
+ color: black
+- icon: branding/amazon
+ url: https://www.amazon.com/Byte-Python-Swaroop-C-H-ebook/dp/B00FJ7S2JU/
+ color: amazon-orange
diff --git a/pydis_site/apps/home/resources/books/effective_python.yaml b/pydis_site/apps/resources/resources/reading/books/effective_python.yaml
index 7f9d0dea..becd0578 100644
--- a/pydis_site/apps/home/resources/books/effective_python.yaml
+++ b/pydis_site/apps/resources/resources/reading/books/effective_python.yaml
@@ -1,14 +1,15 @@
description: A book that gives 90 best practices for writing excellent Python. Great
for intermediates.
name: Effective Python
-payment: paid
+position: 3
urls:
- icon: regular/link
- title: Website
url: https://effectivepython.com/
+ color: teal
- icon: branding/amazon
- title: Amazon
url: https://www.amazon.com/Effective-Python-Specific-Software-Development/dp/0134853989
+ color: amazon-orange
+ title: Amazon
- icon: branding/github
- title: GitHub
url: https://github.com/bslatkin/effectivepython
+ color: black
diff --git a/pydis_site/apps/home/resources/books/flask_web_development.yaml b/pydis_site/apps/resources/resources/reading/books/flask_web_development.yaml
index 613e0e50..cc83a331 100644
--- a/pydis_site/apps/home/resources/books/flask_web_development.yaml
+++ b/pydis_site/apps/resources/resources/reading/books/flask_web_development.yaml
@@ -1,14 +1,14 @@
description: A comprehensive Flask walkthrough that has you building a complete social
blogging application from scratch.
name: Flask Web Development
-payment: paid
+position: 6
urls:
- icon: regular/link
- title: Website
url: http://shop.oreilly.com/product/0636920031116.do
+ color: teal
- icon: branding/amazon
- title: Amazon
url: https://www.amazon.com/Flask-Web-Development-Developing-Applications/dp/1449372627
+ color: amazon-orange
- icon: branding/github
- title: GitHub
url: https://github.com/miguelgrinberg/flasky
+ color: black
diff --git a/pydis_site/apps/home/resources/books/fluent_python.yaml b/pydis_site/apps/resources/resources/reading/books/fluent_python.yaml
index ebfd5f91..92f4bbab 100644
--- a/pydis_site/apps/home/resources/books/fluent_python.yaml
+++ b/pydis_site/apps/resources/resources/reading/books/fluent_python.yaml
@@ -1,14 +1,14 @@
description: A veritable tome of intermediate and advanced Python information. A must-read
- for any Python professional.
+ for any Python professional. By far the most recommended book for intermediates.
name: Fluent Python
-payment: paid
+position: 7
urls:
- icon: regular/link
- title: Website
url: https://www.oreilly.com/library/view/fluent-python/9781491946237/
+ color: teal
- icon: branding/amazon
- title: Amazon
url: https://www.amazon.com/Fluent-Python-Concise-Effective-Programming/dp/1491946008
+ color: amazon-orange
- icon: branding/github
- title: GitHub
url: https://github.com/fluentpython
+ color: black
diff --git a/pydis_site/apps/resources/resources/reading/books/hitchhikers_guide_to_python.yaml b/pydis_site/apps/resources/resources/reading/books/hitchhikers_guide_to_python.yaml
new file mode 100644
index 00000000..906860c7
--- /dev/null
+++ b/pydis_site/apps/resources/resources/reading/books/hitchhikers_guide_to_python.yaml
@@ -0,0 +1,11 @@
+description: A best practice handbook for both novice and expert Python developers to the installation,
+ configuration, and usage of Python on a daily basis.
+name: The Hitchhiker's Guide to Python
+position: 0
+urls:
+- icon: regular/link
+ url: https://python-guide.org/
+ color: teal
+- icon: branding/amazon
+ url: https://www.amazon.com/Hitchhikers-Guide-Python-Practices-Development/dp/1491933178/
+ color: amazon-orange
diff --git a/pydis_site/apps/resources/resources/reading/books/inferential_thinking.yaml b/pydis_site/apps/resources/resources/reading/books/inferential_thinking.yaml
new file mode 100644
index 00000000..27fad4f7
--- /dev/null
+++ b/pydis_site/apps/resources/resources/reading/books/inferential_thinking.yaml
@@ -0,0 +1,9 @@
+description: Inferential Thinking is the textbook for the <a href="http://data8.org/">Foundations of Data Science</a> course at UC Berkley.
+ It introduces you the fundamentals of both Data Science and Python at a level accessible to all.
+ It is available both through your browser and in PDF form.
+name: Inferential Thinking
+position: 13
+urls:
+ - icon: regular/link
+ url: https://www.inferentialthinking.com/chapters/intro
+ color: teal
diff --git a/pydis_site/apps/home/resources/books/mission_python.yaml b/pydis_site/apps/resources/resources/reading/books/mission_python.yaml
index 8cd91979..c4a48b7e 100644
--- a/pydis_site/apps/home/resources/books/mission_python.yaml
+++ b/pydis_site/apps/resources/resources/reading/books/mission_python.yaml
@@ -3,11 +3,11 @@ description: Learn programming and Python while building a complete and awesome
images, and walk-throughs make this a pleasure to both read and follow along. Excellent
book for beginners.
name: Mission Python
-payment: paid
+position: 5
urls:
- icon: regular/link
- title: Website
url: https://www.sean.co.uk/books/mission-python/index.shtm
+ color: teal
- icon: branding/amazon
- title: Amazon
url: https://www.amazon.com/Mission-Python-Code-Space-Adventure/dp/1593278578
+ color: amazon-orange
diff --git a/pydis_site/apps/resources/resources/reading/books/neural_networks_from_scratch_in_python.yaml b/pydis_site/apps/resources/resources/reading/books/neural_networks_from_scratch_in_python.yaml
new file mode 100644
index 00000000..974b0e50
--- /dev/null
+++ b/pydis_site/apps/resources/resources/reading/books/neural_networks_from_scratch_in_python.yaml
@@ -0,0 +1,10 @@
+description: '"Neural Networks From Scratch" is a book intended to teach you how to build neural networks on your own,
+ without any libraries, so you can better understand deep learning and how all of the elements work.
+ This is so you can go out and do new/novel things with deep learning as well as to become more successful with even more basic models.
+ This book is to accompany the usual free tutorial videos and sample code from youtube.com/sentdex.'
+name: Neural Networks from Scratch in Python
+position: 11
+urls:
+ - icon: regular/link
+ url: https://nnfs.io/
+ color: teal
diff --git a/pydis_site/apps/home/resources/books/python_cookbook.yaml b/pydis_site/apps/resources/resources/reading/books/python_cookbook.yaml
index 9fab8e48..032f8c64 100644
--- a/pydis_site/apps/home/resources/books/python_cookbook.yaml
+++ b/pydis_site/apps/resources/resources/reading/books/python_cookbook.yaml
@@ -1,14 +1,14 @@
-description: Complete with 'recipes' for various Python topics, including moving from
- Python 2 to Python 3.3
+description: A book full of very smart problem-solving recipes for various Python topics,
+ including moving from Python 2 to Python 3.
name: Python Cookbook
-payment: paid
+position: 8
urls:
- icon: regular/link
- title: Website
url: http://shop.oreilly.com/product/0636920027072.do
+ color: teal
- icon: branding/amazon
- title: Amazon
url: https://www.amazon.com/Python-Cookbook-Third-David-Beazley/dp/1449340377
+ color: amazon-orange
- icon: branding/github
- title: GitHub
url: https://github.com/dabeaz/python-cookbook
+ color: black
diff --git a/pydis_site/apps/resources/resources/reading/books/python_crash_course.yaml b/pydis_site/apps/resources/resources/reading/books/python_crash_course.yaml
new file mode 100644
index 00000000..3cbf19c8
--- /dev/null
+++ b/pydis_site/apps/resources/resources/reading/books/python_crash_course.yaml
@@ -0,0 +1,20 @@
+description: "This fast-paced, thorough introduction to programming with Python will have you writing programs,
+ solving problems, and making things that work in no time.
+ In the first half of the book, you’ll learn basic programming concepts, such as variables, lists, classes, and loops,
+ and practice writing clean code with exercises for each topic.
+ You’ll also learn how to make your programs interactive and test your code safely before adding it to a project.
+ In the second half, you’ll put your new knowledge into practice with three substantial projects:
+ a Space Invaders–inspired arcade game, a set of data visualizations with Python’s handy libraries,
+ and a simple web app you can deploy online."
+name: Python Crash Course
+position: 12
+urls:
+ - icon: regular/link
+ url: https://nostarch.com/pythoncrashcourse2e
+ color: teal
+ - icon: branding/amazon
+ url: https://www.amazon.com/Python-Crash-Course-Project-Based-Introduction/dp/1593276036
+ color: amazon-orange
+ - icon: branding/github
+ url: https://ehmatthes.github.io/pcc/
+ color: black
diff --git a/pydis_site/apps/home/resources/books/python_tricks.yaml b/pydis_site/apps/resources/resources/reading/books/python_tricks.yaml
index 0638058c..c0941809 100644
--- a/pydis_site/apps/home/resources/books/python_tricks.yaml
+++ b/pydis_site/apps/resources/resources/reading/books/python_tricks.yaml
@@ -2,11 +2,11 @@ description: Full of useful Python tips, tricks and features. Get this if you ha
a good grasp of the basics and want to take your Python skills to the next level,
or are a experienced programmer looking to add to your toolbelt.
name: Python Tricks
-payment: paid
+position: 4
urls:
- icon: regular/link
- title: Website
url: https://realpython.com/products/python-tricks-book/
+ color: teal
- icon: branding/amazon
- title: Amazon
url: https://www.amazon.com/Python-Tricks-Buffet-Awesome-Features/dp/1775093301
+ color: amazon-orange
diff --git a/pydis_site/apps/resources/resources/reading/books/think_python.yaml b/pydis_site/apps/resources/resources/reading/books/think_python.yaml
new file mode 100644
index 00000000..6de87043
--- /dev/null
+++ b/pydis_site/apps/resources/resources/reading/books/think_python.yaml
@@ -0,0 +1,17 @@
+description: Think Python is an introduction to Python programming for beginners.
+ It starts with basic concepts of programming,
+ and is carefully designed to define all terms when they are first used and to develop each new concept in a logical progression.
+ Larger pieces, like recursion and object-oriented programming are divided into a sequence of smaller steps
+ and introduced over the course of several chapters.
+name: Think Python
+position: 10
+urls:
+ - icon: regular/link
+ url: https://greenteapress.com/wp/think-python-2e/
+ color: teal
+ - icon: branding/amazon
+ url: https://www.amazon.com/gp/product/1491939362
+ color: amazon-orange
+ - icon: branding/github
+ url: https://github.com/AllenDowney/ThinkPython2
+ color: black
diff --git a/pydis_site/apps/home/resources/books/two_scoops_of_django.yaml b/pydis_site/apps/resources/resources/reading/books/two_scoops_of_django.yaml
index 85cfa0fc..7d83e7c4 100644
--- a/pydis_site/apps/home/resources/books/two_scoops_of_django.yaml
+++ b/pydis_site/apps/resources/resources/reading/books/two_scoops_of_django.yaml
@@ -1,14 +1,14 @@
-description: This book is chock-full of material that will help you with your Django
- projects.
+description: Tips, tricks, and best practices for your Django project.
+ A highly recommended resource for Django web developers.
name: Two Scoops of Django
-payment: paid
+position: 9
urls:
- icon: regular/link
- title: Website
url: https://twoscoopspress.com/products/two-scoops-of-django-1-11
+ color: teal
- icon: branding/amazon
- title: Amazon
url: https://www.amazon.com/Two-Scoops-Django-Best-Practices/dp/0981467342
+ color: amazon-orange
- icon: branding/github
- title: GitHub
url: https://github.com/twoscoops/two-scoops-of-django-2.0-code-examples
+ color: black
diff --git a/pydis_site/apps/resources/resources/reading/tutorials/_category_info.yaml b/pydis_site/apps/resources/resources/reading/tutorials/_category_info.yaml
new file mode 100644
index 00000000..a18b837d
--- /dev/null
+++ b/pydis_site/apps/resources/resources/reading/tutorials/_category_info.yaml
@@ -0,0 +1,5 @@
+description: Tutorials and references for those that are just getting started with Python.
+name: Tutorials
+default_icon: branding/python
+default_icon_color: black
+position: 1
diff --git a/pydis_site/apps/resources/resources/reading/tutorials/getting_started_with_kivy.yaml b/pydis_site/apps/resources/resources/reading/tutorials/getting_started_with_kivy.yaml
new file mode 100644
index 00000000..d1d9a7d2
--- /dev/null
+++ b/pydis_site/apps/resources/resources/reading/tutorials/getting_started_with_kivy.yaml
@@ -0,0 +1,5 @@
+description: A big list of excellent resources for getting started making Kivy applications.
+name: Getting Started with Kivy
+title_url: https://blog.kivy.org/2019/12/getting-started-with-kivy/
+icon_image: https://raw.githubusercontent.com/kivy/kivy-website/master/logos/kivy-logo-black-256.png
+position: 3
diff --git a/pydis_site/apps/resources/resources/reading/tutorials/getting_started_with_python_for_non_programmers.yaml b/pydis_site/apps/resources/resources/reading/tutorials/getting_started_with_python_for_non_programmers.yaml
new file mode 100644
index 00000000..3250a7c4
--- /dev/null
+++ b/pydis_site/apps/resources/resources/reading/tutorials/getting_started_with_python_for_non_programmers.yaml
@@ -0,0 +1,5 @@
+description: A list of beginner resources for programmers with no prior developer experience,
+ from Python's official guide.
+name: Getting Started with Python for Non-Programmers
+title_url: https://wiki.python.org/moin/BeginnersGuide/NonProgrammers
+position: 1
diff --git a/pydis_site/apps/resources/resources/reading/tutorials/getting_started_with_python_for_programmers.yaml b/pydis_site/apps/resources/resources/reading/tutorials/getting_started_with_python_for_programmers.yaml
new file mode 100644
index 00000000..b65e0e12
--- /dev/null
+++ b/pydis_site/apps/resources/resources/reading/tutorials/getting_started_with_python_for_programmers.yaml
@@ -0,0 +1,5 @@
+description: A list of beginner resources for programmers coming from other languages,
+ from Python's official guide.
+name: Getting Started with Python for Programmers
+title_url: https://wiki.python.org/moin/BeginnersGuide/Programmers
+position: 0
diff --git a/pydis_site/apps/resources/resources/reading/tutorials/python_cheat_sheet.yaml b/pydis_site/apps/resources/resources/reading/tutorials/python_cheat_sheet.yaml
new file mode 100644
index 00000000..70ac49ef
--- /dev/null
+++ b/pydis_site/apps/resources/resources/reading/tutorials/python_cheat_sheet.yaml
@@ -0,0 +1,5 @@
+description: A Python 3 cheat sheet with useful information and tips, as well as common
+ pitfalls for beginners. This is a PDF.
+name: Python Cheat Sheet
+title_url: https://perso.limsi.fr/pointal/_media/python:cours:mementopython3-english.pdf
+position: 6
diff --git a/pydis_site/apps/resources/resources/reading/tutorials/python_developer_guide.yaml b/pydis_site/apps/resources/resources/reading/tutorials/python_developer_guide.yaml
new file mode 100644
index 00000000..625d57c8
--- /dev/null
+++ b/pydis_site/apps/resources/resources/reading/tutorials/python_developer_guide.yaml
@@ -0,0 +1,5 @@
+description: This guide is a comprehensive resource for contributing to Python – for both new and experienced contributors.
+ It is maintained by the same community that maintains Python.
+name: Python Developer's Guide
+title_url: https://devguide.python.org/
+position: 2
diff --git a/pydis_site/apps/resources/resources/reading/tutorials/simple_guide_to_git.yaml b/pydis_site/apps/resources/resources/reading/tutorials/simple_guide_to_git.yaml
new file mode 100644
index 00000000..a505715d
--- /dev/null
+++ b/pydis_site/apps/resources/resources/reading/tutorials/simple_guide_to_git.yaml
@@ -0,0 +1,6 @@
+description: A simple, no-nonsense guide to the basics of using Git.
+name: A Simple Guide to Git
+title_url: http://rogerdudler.github.io/git-guide/
+title_icon: branding/github
+title_icon_color: black
+position: 4
diff --git a/pydis_site/apps/resources/resources/reading/tutorials/the_flask_mega_tutorial.yaml b/pydis_site/apps/resources/resources/reading/tutorials/the_flask_mega_tutorial.yaml
new file mode 100644
index 00000000..8d61ea73
--- /dev/null
+++ b/pydis_site/apps/resources/resources/reading/tutorials/the_flask_mega_tutorial.yaml
@@ -0,0 +1,4 @@
+description: Miguel Grinberg's fully featured mega-tutorial for learning how to create web applications with the Flask framework.
+name: The Flask Mega-Tutorial
+title_url: https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world
+position: 5
diff --git a/pydis_site/apps/resources/resources/reading/tutorials/wtf_python.yaml b/pydis_site/apps/resources/resources/reading/tutorials/wtf_python.yaml
new file mode 100644
index 00000000..a25a84fd
--- /dev/null
+++ b/pydis_site/apps/resources/resources/reading/tutorials/wtf_python.yaml
@@ -0,0 +1,8 @@
+description: Python, being a beautifully designed high-level and interpreter-based programming language,
+ provides us with many features for the programmer's comfort.
+ But sometimes, the outcomes of a Python snippet may not seem obvious at first sight.
+ Here's a fun project attempting to explain what exactly is happening under the hood for some counter-intuitive snippets
+ and lesser-known features in Python.
+name: WTF Python
+title_url: https://github.com/satwikkansal/wtfpython
+position: 7
diff --git a/pydis_site/apps/resources/resources/tools/_category_info.yaml b/pydis_site/apps/resources/resources/tools/_category_info.yaml
new file mode 100644
index 00000000..6b16baa6
--- /dev/null
+++ b/pydis_site/apps/resources/resources/tools/_category_info.yaml
@@ -0,0 +1,4 @@
+description: This page is a curated list of tools that we regularly recommend in the community.
+ If you have a suggestion for something to add to this page, please create an issue in
+ <a href="https://github.com/python-discord/meta/issues">our meta repo</a>, and we'll consider adding it.
+name: Tools
diff --git a/pydis_site/apps/resources/resources/tools/accessibility_tools/_category_info.yaml b/pydis_site/apps/resources/resources/tools/accessibility_tools/_category_info.yaml
new file mode 100644
index 00000000..e770db07
--- /dev/null
+++ b/pydis_site/apps/resources/resources/tools/accessibility_tools/_category_info.yaml
@@ -0,0 +1,5 @@
+description: Accessibility tools that help people write Python code.
+name: Accessibility Tools
+default_icon: branding/python
+default_icon_color: black
+position: 2
diff --git a/pydis_site/apps/resources/resources/tools/accessibility_tools/screen_readers.yaml b/pydis_site/apps/resources/resources/tools/accessibility_tools/screen_readers.yaml
new file mode 100644
index 00000000..39372956
--- /dev/null
+++ b/pydis_site/apps/resources/resources/tools/accessibility_tools/screen_readers.yaml
@@ -0,0 +1,7 @@
+description: Screen readers are software programs that allow blind
+ or visually impaired users to read the text displayed on a computer screen with a speech synthesizer or braille display.
+ There are many different screen reader program options,
+ with this link describing many of them and their capabilities.
+name: Screen Readers - American Foundation for the Blind
+title_url: https://www.afb.org/blindness-and-low-vision/using-technology/assistive-technology-products/screen-readers
+position: 1
diff --git a/pydis_site/apps/resources/resources/tools/accessibility_tools/talon_voice.yaml b/pydis_site/apps/resources/resources/tools/accessibility_tools/talon_voice.yaml
new file mode 100644
index 00000000..9df5f66f
--- /dev/null
+++ b/pydis_site/apps/resources/resources/tools/accessibility_tools/talon_voice.yaml
@@ -0,0 +1,6 @@
+description: Talon is a tool being built that aims to bring programming,
+ realtime video gaming, command line, and full desktop computer proficiency to people
+ who have limited or no use of their hands.
+name: Talon Voice
+title_url: https://talonvoice.com/
+position: 0
diff --git a/pydis_site/apps/home/resources/editors/_category_info.yaml b/pydis_site/apps/resources/resources/tools/editors/_category_info.yaml
index f8dc1413..3cdfff3a 100644
--- a/pydis_site/apps/home/resources/editors/_category_info.yaml
+++ b/pydis_site/apps/resources/resources/tools/editors/_category_info.yaml
@@ -1,2 +1,5 @@
description: Lightweight code editors supporting Python
name: Editors
+default_icon: branding/python
+default_icon_color: black
+position: 1
diff --git a/pydis_site/apps/resources/resources/tools/editors/atom.yaml b/pydis_site/apps/resources/resources/tools/editors/atom.yaml
new file mode 100644
index 00000000..c44f9b5b
--- /dev/null
+++ b/pydis_site/apps/resources/resources/tools/editors/atom.yaml
@@ -0,0 +1,5 @@
+description: A free Electron-based editor, a "hackable text editor for the 21st century", maintained
+ by the GitHub team.
+name: Atom
+title_url: https://atom.io/
+position: 0
diff --git a/pydis_site/apps/resources/resources/tools/editors/google_collab.yaml b/pydis_site/apps/resources/resources/tools/editors/google_collab.yaml
new file mode 100644
index 00000000..302c3e2e
--- /dev/null
+++ b/pydis_site/apps/resources/resources/tools/editors/google_collab.yaml
@@ -0,0 +1,7 @@
+description: Google Collab is a high-powered custom version of Jupyter Notebook which supports e.g.
+ !apt-get to install arbitrary Debian packages to the runtime, which is very generous with CPU and memory,
+ and well-integrated with Google Drive.
+ You can share your Collab Notebooks with other people and work collaboratively.
+name: Google Collab
+title_url: https://colab.research.google.com/notebooks/intro.ipynb
+position: 4
diff --git a/pydis_site/apps/resources/resources/tools/editors/mu_editor.yaml b/pydis_site/apps/resources/resources/tools/editors/mu_editor.yaml
new file mode 100644
index 00000000..b92bac9d
--- /dev/null
+++ b/pydis_site/apps/resources/resources/tools/editors/mu_editor.yaml
@@ -0,0 +1,7 @@
+description: An editor aimed at beginners for the purpose of learning how to code
+ without the distractions more advanced editors sometimes cause.
+ Particularly useful for use with microcontrollers,
+ with built-in tools to interact with Adafruit and Arduino boards.
+name: Mu-Editor
+title_url: https://codewith.mu/
+position: 3
diff --git a/pydis_site/apps/resources/resources/tools/editors/sublime_text.yaml b/pydis_site/apps/resources/resources/tools/editors/sublime_text.yaml
new file mode 100644
index 00000000..3c6e7e84
--- /dev/null
+++ b/pydis_site/apps/resources/resources/tools/editors/sublime_text.yaml
@@ -0,0 +1,5 @@
+description: A powerful Python-backed editor with great community support and a wealth
+ of extensions.
+name: Sublime Text
+title_url: https://www.sublimetext.com/
+position: 2
diff --git a/pydis_site/apps/resources/resources/tools/editors/visual_studio_code.yaml b/pydis_site/apps/resources/resources/tools/editors/visual_studio_code.yaml
new file mode 100644
index 00000000..e3737ca7
--- /dev/null
+++ b/pydis_site/apps/resources/resources/tools/editors/visual_studio_code.yaml
@@ -0,0 +1,4 @@
+description: A fully-featured editor based on Electron, extendable with plugins.
+name: Visual Studio Code
+title_url: https://code.visualstudio.com/
+position: 1
diff --git a/pydis_site/apps/resources/resources/tools/ides/_category_info.yaml b/pydis_site/apps/resources/resources/tools/ides/_category_info.yaml
new file mode 100644
index 00000000..614625a6
--- /dev/null
+++ b/pydis_site/apps/resources/resources/tools/ides/_category_info.yaml
@@ -0,0 +1,5 @@
+description: Fully-integrated development environments for serious Python work.
+name: IDEs
+default_icon: branding/python
+default_icon_color: black
+position: 0
diff --git a/pydis_site/apps/resources/resources/tools/ides/pycharm.yaml b/pydis_site/apps/resources/resources/tools/ides/pycharm.yaml
new file mode 100644
index 00000000..b959b0f8
--- /dev/null
+++ b/pydis_site/apps/resources/resources/tools/ides/pycharm.yaml
@@ -0,0 +1,5 @@
+description: The very best Python IDE, with a wealth of advanced features and convenience
+ functions.
+name: PyCharm
+title_url: https://www.jetbrains.com/pycharm/
+position: 0
diff --git a/pydis_site/apps/resources/resources/tools/ides/repl_it.yaml b/pydis_site/apps/resources/resources/tools/ides/repl_it.yaml
new file mode 100644
index 00000000..8cd14e14
--- /dev/null
+++ b/pydis_site/apps/resources/resources/tools/ides/repl_it.yaml
@@ -0,0 +1,5 @@
+description: A free, collaborative, in-browser IDE to code in 50+ languages —
+ without spending a second on setup.
+name: repl.it
+title_url: https://repl.it/
+position: 3
diff --git a/pydis_site/apps/resources/resources/tools/ides/spyder.yaml b/pydis_site/apps/resources/resources/tools/ides/spyder.yaml
new file mode 100644
index 00000000..c2f9c2dc
--- /dev/null
+++ b/pydis_site/apps/resources/resources/tools/ides/spyder.yaml
@@ -0,0 +1,5 @@
+description: The Scientific Python Development Environment.
+ Simpler and lighter than PyCharm, but still packs a punch.
+name: Spyder
+title_url: https://www.spyder-ide.org/
+position: 1
diff --git a/pydis_site/apps/resources/resources/tools/ides/thonny.yaml b/pydis_site/apps/resources/resources/tools/ides/thonny.yaml
new file mode 100644
index 00000000..3581e1cd
--- /dev/null
+++ b/pydis_site/apps/resources/resources/tools/ides/thonny.yaml
@@ -0,0 +1,5 @@
+description: A Python IDE specifially aimed at learning programming. Has a lot of
+ helpful features to help you understand your code.
+name: Thonny
+title_url: https://thonny.org/
+position: 2
diff --git a/pydis_site/apps/resources/resources/videos/_category_info.yaml b/pydis_site/apps/resources/resources/videos/_category_info.yaml
new file mode 100644
index 00000000..8192e021
--- /dev/null
+++ b/pydis_site/apps/resources/resources/videos/_category_info.yaml
@@ -0,0 +1,2 @@
+description: Excellent Youtube channels with content related to Python.
+name: Videos
diff --git a/pydis_site/apps/resources/resources/videos/corey_schafer.yaml b/pydis_site/apps/resources/resources/videos/corey_schafer.yaml
new file mode 100644
index 00000000..a7cca18a
--- /dev/null
+++ b/pydis_site/apps/resources/resources/videos/corey_schafer.yaml
@@ -0,0 +1,19 @@
+description: 'Corey has a number of exceptionally high quality tutorial series
+ on everything from Python basics to Django and Flask:
+ <ul>
+ <li><a href="https://www.youtube.com/playlist?list=PL-osiE80TeTskrapNbzXhwoFUiLCjGgY7">Python Programming Beginner Tutorials</a></li>
+ <li><a href="https://www.youtube.com/playlist?list=PL-osiE80TeTsqhIuOqKhwlXsIBIdSeYtc">Python OOP Tutorials - Working With Classes</a></li>
+ <li><a href="https://www.youtube.com/playlist?list=PL-osiE80TeTs4UjLw5MM6OjgkjFeUxCYH">Flask Tutorials</a></li>
+ <li><a href="https://www.youtube.com/playlist?list=PL-osiE80TeTtoQCKZ03TU5fNfx2UY6U4p">Django Tutorials</a></li>
+ </ul>
+ Check out his channel for more video series!
+ '
+title_image: https://i.imgur.com/KIfWw3b.png
+position: 0
+urls:
+ - icon: branding/youtube
+ url: https://www.youtube.com/channel/UCCezIgC97PvUuR4_gbFUs5g
+ color: youtube-red
+ - icon: regular/link
+ url: https://coreyms.com/
+ color: teal
diff --git a/pydis_site/apps/resources/resources/videos/jetbrains.yaml b/pydis_site/apps/resources/resources/videos/jetbrains.yaml
new file mode 100644
index 00000000..5d130db6
--- /dev/null
+++ b/pydis_site/apps/resources/resources/videos/jetbrains.yaml
@@ -0,0 +1,12 @@
+description: A collection of videos made by the PyCharm team at JetBrains on subjects such as TDD,
+ Django, pytest and much more!<br><br>
+ Episodes of their "What does this package do?" series go over all sorts of libraries in Python
+ both in the standard library and from the community and give a video explanation of the key concepts.
+icon_image: https://upload.wikimedia.org/wikipedia/commons/thumb/1/1a/JetBrains_Logo_2016.svg/1200px-JetBrains_Logo_2016.svg.png
+icon_size: 50
+title_image: https://resources.jetbrains.com/storage/products/pycharm/img/meta/pycharm_logo_300x300.png
+position: 3
+urls:
+ - icon: branding/youtube
+ url: https://www.youtube.com/channel/UCak6beUTLlVmf0E4AmnQkmw
+ color: youtube-red
diff --git a/pydis_site/apps/resources/resources/videos/jim_shaped_coding.yaml b/pydis_site/apps/resources/resources/videos/jim_shaped_coding.yaml
new file mode 100644
index 00000000..488cfa83
--- /dev/null
+++ b/pydis_site/apps/resources/resources/videos/jim_shaped_coding.yaml
@@ -0,0 +1,13 @@
+description: 'JimShapedCoding contains a set of YouTube tutorials covering things from Flask to GUI development in Python:
+ <ul>
+ <li><a href="https://www.youtube.com/playlist?list=PLOkVupluCIjuPtTkhO6jmA76uQR994Wvi">Flask tutorials</a></li>
+ <li><a href="https://www.youtube.com/watch?v=0tqZ6rMcqGE&list=PLOkVupluCIjuAzAmDNUXcD-0Fsb8sbv9F">GUI tutorials</a></li>
+ <li><a href="https://www.youtube.com/watch?v=qMrAFscMBBc&list=PLOkVupluCIjvORWaF4kG-sXLgbVemYpEi">Django tutorials</a></li>
+ </ul>
+ Check out his channel for more videos!'
+title_image: https://i.imgur.com/DlovZPf.png
+position: 5
+urls:
+ - icon: branding/youtube
+ url: https://www.youtube.com/channel/UCU8d7rcShA7MGuDyYH1aWGg
+ color: youtube-red
diff --git a/pydis_site/apps/resources/resources/videos/microsoft.yaml b/pydis_site/apps/resources/resources/videos/microsoft.yaml
new file mode 100644
index 00000000..720ee202
--- /dev/null
+++ b/pydis_site/apps/resources/resources/videos/microsoft.yaml
@@ -0,0 +1,18 @@
+description: A trove of tutorials & guides for developers from Microsoft's Developer hub.
+ Follow the links below for a series of high-quality Python tutorials for beginners.
+ <ul>
+ <li><a href="https://www.youtube.com/playlist?list=PLlrxD0HtieHhS8VzuMCfQD4uJ9yne1mE6">Python for Beginners</a></li>
+ <li><a href="https://www.youtube.com/playlist?list=PLlrxD0HtieHiXd-nEby-TMCoUNwhbLUnj">More Python for Beginners</a></li>
+ <li><a href="https://www.youtube.com/playlist?list=PLlrxD0HtieHhHnCUVtR8UHS7eLl33zfJ-">Even More Python for Beginners - Data Tools</a></li>
+ </ul>
+ Microsoft's Python Development Team also runs a Discord Server for discussions of Python in the Microsoft ecosystem,
+ including Visual Studio Code and Azure.
+title_image: http://img-prod-cms-rt-microsoft-com.akamaized.net/cms/api/am/imageFileData/RE2qVsJ?ver=3f74
+position: 4
+urls:
+ - icon: branding/youtube
+ url: https://www.youtube.com/channel/UCsMica-v34Irf9KVTh6xx-g
+ color: youtube-red
+ - icon: branding/discord
+ url: https://aka.ms/python-discord
+ color: blurple
diff --git a/pydis_site/apps/resources/resources/videos/python_discord.yaml b/pydis_site/apps/resources/resources/videos/python_discord.yaml
new file mode 100644
index 00000000..04235b08
--- /dev/null
+++ b/pydis_site/apps/resources/resources/videos/python_discord.yaml
@@ -0,0 +1,8 @@
+description: It's our channel! We are slowly gathering content here directly related to Python,
+ our community and the events we host. Come check us out!
+title_image: https://raw.githubusercontent.com/python-discord/branding/master/logos/logo_banner/logo_site_banner_dark_512.png
+position: 2
+urls:
+ - icon: branding/youtube
+ url: https://www.youtube.com/pythondiscord
+ color: youtube-red
diff --git a/pydis_site/apps/resources/resources/videos/sentdex.yaml b/pydis_site/apps/resources/resources/videos/sentdex.yaml
new file mode 100644
index 00000000..4e5f54c6
--- /dev/null
+++ b/pydis_site/apps/resources/resources/videos/sentdex.yaml
@@ -0,0 +1,22 @@
+description: 'An enormous amount of Python content for all skill levels
+ from the most popular Python YouTuber on the web.
+ <ul>
+ <li><a href="https://www.youtube.com/playlist?list=PLQVvvaa0QuDeAams7fkdcwOGBpGdHpXln">Learning to program with Python 3 (py 3.7)</a></li>
+ <li><a href="https://www.youtube.com/playlist?list=PLQVvvaa0QuDfwnDTZWw8H3hN_VRQfq8rF">Kivy - Mobile and Desktop App Dev w/ Python</a></li>
+ <li><a href="https://www.youtube.com/playlist?list=PLQVvvaa0QuDfSfqQuee6K8opKtZsh7sA9">Data Analysis w/ Python 3 and Pandas</a></li>
+ <li><a href="https://www.youtube.com/playlist?list=PLQVvvaa0QuDfKTOs3Keq_kaG2P55YRn5v">Machine Learning with Python</a></li>
+ </ul>
+ Check out his channel for more video series!
+ '
+title_image: https://i.imgur.com/kJgWZIu.png
+position: 1
+urls:
+ - icon: branding/youtube
+ url: https://www.youtube.com/user/sentdex
+ color: youtube-red
+ - icon: branding/discord
+ url: https://discord.gg/sentdex
+ color: blurple
+ - icon: regular/link
+ url: https://pythonprogramming.net/
+ color: teal
diff --git a/pydis_site/apps/resources/templatetags/__init__.py b/pydis_site/apps/resources/templatetags/__init__.py
new file mode 100644
index 00000000..2b266b94
--- /dev/null
+++ b/pydis_site/apps/resources/templatetags/__init__.py
@@ -0,0 +1,3 @@
+from .as_icon import as_icon
+
+__all__ = ["as_icon"]
diff --git a/pydis_site/apps/resources/templatetags/as_icon.py b/pydis_site/apps/resources/templatetags/as_icon.py
new file mode 100644
index 00000000..b211407c
--- /dev/null
+++ b/pydis_site/apps/resources/templatetags/as_icon.py
@@ -0,0 +1,14 @@
+from django import template
+
+register = template.Library()
+
+
+def as_icon(icon: str) -> str:
+ """Convert icon string in format 'type/icon' to fa-icon HTML classes."""
+ icon_type, icon_name = icon.split("/")
+ if icon_type.lower() == "branding":
+ icon_type = "fab"
+ else:
+ icon_type = "fas"
+ return f'{icon_type} fa-{icon_name}'
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_as_icon.py b/pydis_site/apps/resources/tests/test_as_icon.py
new file mode 100644
index 00000000..5b33910d
--- /dev/null
+++ b/pydis_site/apps/resources/tests/test_as_icon.py
@@ -0,0 +1,28 @@
+from django.test import TestCase
+
+from pydis_site.apps.resources.templatetags import as_icon
+
+
+class TestAsIcon(TestCase):
+ """Tests for `as_icon` templatetag."""
+
+ def test_as_icon(self):
+ """Should return proper icon type class and icon class based on input."""
+ test_cases = [
+ {
+ "input": "regular/icon",
+ "output": "fas fa-icon",
+ },
+ {
+ "input": "branding/brand",
+ "output": "fab fa-brand",
+ },
+ {
+ "input": "fake/my-icon",
+ "output": "fas fa-my-icon",
+ }
+ ]
+
+ for case in test_cases:
+ with self.subTest(input=case["input"], output=case["output"]):
+ self.assertEqual(case["output"], as_icon(case["input"]))
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..53685eef
--- /dev/null
+++ b/pydis_site/apps/resources/tests/test_views.py
@@ -0,0 +1,34 @@
+from pathlib import Path
+from unittest.mock import patch
+
+from django.conf import settings
+from django.test import TestCase
+from django_hosts import reverse
+
+TESTING_RESOURCES_PATH = Path(
+ settings.BASE_DIR, "pydis_site", "apps", "resources", "tests", "testing_resources"
+)
+
+
+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)
+
+
+class TestResourcesListView(TestCase):
+ @patch("pydis_site.apps.resources.views.resources_list.RESOURCES_PATH", TESTING_RESOURCES_PATH)
+ def test_valid_resource_list_200(self):
+ """Check does site return code 200 when visiting valid resource list."""
+ url = reverse("resources:resources", ("testing",))
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+
+ @patch("pydis_site.apps.resources.views.resources_list.RESOURCES_PATH", TESTING_RESOURCES_PATH)
+ def test_invalid_resource_list_404(self):
+ """Check does site return code 404 when trying to visit invalid resource list."""
+ url = reverse("resources:resources", ("invalid",))
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 404)
diff --git a/pydis_site/apps/resources/tests/testing_resources/testing/_category_info.yaml b/pydis_site/apps/resources/tests/testing_resources/testing/_category_info.yaml
new file mode 100644
index 00000000..bae17ea3
--- /dev/null
+++ b/pydis_site/apps/resources/tests/testing_resources/testing/_category_info.yaml
@@ -0,0 +1 @@
+name: Testing
diff --git a/pydis_site/apps/resources/tests/testing_resources/testing/foobar/_category_info.yaml b/pydis_site/apps/resources/tests/testing_resources/testing/foobar/_category_info.yaml
new file mode 100644
index 00000000..eaac32d9
--- /dev/null
+++ b/pydis_site/apps/resources/tests/testing_resources/testing/foobar/_category_info.yaml
@@ -0,0 +1 @@
+name: Foobar
diff --git a/pydis_site/apps/resources/tests/testing_resources/testing/foobar/resource_test.yaml b/pydis_site/apps/resources/tests/testing_resources/testing/foobar/resource_test.yaml
new file mode 100644
index 00000000..22835090
--- /dev/null
+++ b/pydis_site/apps/resources/tests/testing_resources/testing/foobar/resource_test.yaml
@@ -0,0 +1 @@
+name: Resource Test
diff --git a/pydis_site/apps/resources/tests/testing_resources/testing/my_resource.yaml b/pydis_site/apps/resources/tests/testing_resources/testing/my_resource.yaml
new file mode 100644
index 00000000..61df6173
--- /dev/null
+++ b/pydis_site/apps/resources/tests/testing_resources/testing/my_resource.yaml
@@ -0,0 +1 @@
+name: My Resource
diff --git a/pydis_site/apps/resources/urls.py b/pydis_site/apps/resources/urls.py
new file mode 100644
index 00000000..19142081
--- /dev/null
+++ b/pydis_site/apps/resources/urls.py
@@ -0,0 +1,9 @@
+from django.urls import path
+
+from pydis_site.apps.resources import views
+
+app_name = "resources"
+urlpatterns = [
+ path("", views.ResourcesView.as_view(), name="index"),
+ path("<str:category>/", views.ResourcesListView.as_view(), name="resources")
+]
diff --git a/pydis_site/apps/resources/utils.py b/pydis_site/apps/resources/utils.py
new file mode 100644
index 00000000..1855fc80
--- /dev/null
+++ b/pydis_site/apps/resources/utils.py
@@ -0,0 +1,42 @@
+import typing as t
+from pathlib import Path
+
+import yaml
+
+
+def get_resources(path: Path) -> t.List[t.Dict]:
+ """Loads resource YAMLs from provided path."""
+ resources = []
+
+ for item in path.iterdir():
+ if item.is_file() and item.suffix == ".yaml" and item.name != "_category_info.yaml":
+ resources.append(yaml.safe_load(item.read_text()))
+
+ return resources
+
+
+def get_subcategories(path: Path) -> t.List[t.Dict]:
+ """Loads resources subcategories with their resources by provided path."""
+ subcategories = []
+
+ for item in path.iterdir():
+ if item.is_dir() and item.joinpath("_category_info.yaml").exists():
+ subcategories.append({
+ "category_info": {
+ **yaml.safe_load(
+ item.joinpath("_category_info.yaml").read_text()
+ ),
+ "raw_name": item.name
+ },
+ "resources": [
+ yaml.safe_load(subitem.read_text())
+ for subitem in item.iterdir()
+ if (
+ subitem.is_file()
+ and subitem.suffix == ".yaml"
+ and subitem.name != "_category_info.yaml"
+ )
+ ]
+ })
+
+ return subcategories
diff --git a/pydis_site/apps/resources/views/__init__.py b/pydis_site/apps/resources/views/__init__.py
new file mode 100644
index 00000000..8eb383b5
--- /dev/null
+++ b/pydis_site/apps/resources/views/__init__.py
@@ -0,0 +1,4 @@
+from .resources import ResourcesView
+from .resources_list import ResourcesListView
+
+__all__ = ["ResourcesView", "ResourcesListView"]
diff --git a/pydis_site/apps/resources/views/resources.py b/pydis_site/apps/resources/views/resources.py
new file mode 100644
index 00000000..25ce3e50
--- /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/resources/views/resources_list.py b/pydis_site/apps/resources/views/resources_list.py
new file mode 100644
index 00000000..55f22993
--- /dev/null
+++ b/pydis_site/apps/resources/views/resources_list.py
@@ -0,0 +1,39 @@
+from pathlib import Path
+from typing import Any, Dict
+
+import yaml
+from django.conf import settings
+from django.http import Http404
+from django.views.generic import TemplateView
+
+from pydis_site.apps.resources.utils import get_resources, get_subcategories
+
+RESOURCES_PATH = Path(settings.BASE_DIR, "pydis_site", "apps", "resources", "resources")
+
+
+class ResourcesListView(TemplateView):
+ """Shows specific resources list."""
+
+ template_name = "resources/resources_list.html"
+
+ def get_context_data(self, **kwargs) -> Dict[str, Any]:
+ """Add resources and subcategories data into context."""
+ context = super().get_context_data(**kwargs)
+
+ resource_path = RESOURCES_PATH / self.kwargs["category"]
+ if (
+ not resource_path.is_dir()
+ or not resource_path.joinpath("_category_info.yaml").exists()
+ ):
+ raise Http404
+
+ context["resources"] = get_resources(resource_path)
+ context["subcategories"] = get_subcategories(resource_path)
+ context["category_info"] = {
+ **yaml.safe_load(
+ resource_path.joinpath("_category_info.yaml").read_text()
+ ),
+ "raw_name": resource_path.name
+ }
+
+ return context
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/apps/staff/urls.py b/pydis_site/apps/staff/urls.py
index a564d516..ca8d1a0f 100644
--- a/pydis_site/apps/staff/urls.py
+++ b/pydis_site/apps/staff/urls.py
@@ -1,5 +1,3 @@
-from django.conf import settings
-from django.conf.urls.static import static
from django.urls import path
from .viewsets import LogView
@@ -7,4 +5,4 @@ from .viewsets import LogView
app_name = 'staff'
urlpatterns = [
path('bot/logs/<int:pk>/', LogView.as_view(), name="logs"),
-] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
+]