aboutsummaryrefslogtreecommitdiffstats
path: root/pydis_site/apps/home
diff options
context:
space:
mode:
Diffstat (limited to 'pydis_site/apps/home')
-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/__init__.py0
-rw-r--r--pydis_site/apps/home/forms/account_deletion.py10
-rw-r--r--pydis_site/apps/home/resources/books/_category_info.yaml2
-rw-r--r--pydis_site/apps/home/resources/books/automate_the_boring_stuff.yaml16
-rw-r--r--pydis_site/apps/home/resources/books/byte_of_python.yaml17
-rw-r--r--pydis_site/apps/home/resources/books/effective_python.yaml14
-rw-r--r--pydis_site/apps/home/resources/books/flask_web_development.yaml14
-rw-r--r--pydis_site/apps/home/resources/books/fluent_python.yaml14
-rw-r--r--pydis_site/apps/home/resources/books/mission_python.yaml13
-rw-r--r--pydis_site/apps/home/resources/books/python_cookbook.yaml14
-rw-r--r--pydis_site/apps/home/resources/books/python_tricks.yaml12
-rw-r--r--pydis_site/apps/home/resources/books/two_scoops_of_django.yaml14
-rw-r--r--pydis_site/apps/home/resources/communities/_category_info.yaml2
-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/_category_info.yaml2
-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/code_combat.yaml13
-rw-r--r--pydis_site/apps/home/resources/interactive_learning_tools/exercism.yaml14
-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/interactive_learning_tools/python_morsels.yaml15
-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/podcasts/podcast_dunder_init.yaml9
-rw-r--r--pydis_site/apps/home/resources/podcasts/python_bytes.yaml9
-rw-r--r--pydis_site/apps/home/resources/podcasts/talk_python_to_me.yaml9
-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
62 files changed, 9 insertions, 2010 deletions
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/__init__.py b/pydis_site/apps/home/forms/__init__.py
deleted file mode 100644
index e69de29b..00000000
--- a/pydis_site/apps/home/forms/__init__.py
+++ /dev/null
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/_category_info.yaml b/pydis_site/apps/home/resources/books/_category_info.yaml
deleted file mode 100644
index e3b89ad3..00000000
--- a/pydis_site/apps/home/resources/books/_category_info.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
-description: The best books for learning Python or Python Frameworks
-name: Books
diff --git a/pydis_site/apps/home/resources/books/automate_the_boring_stuff.yaml b/pydis_site/apps/home/resources/books/automate_the_boring_stuff.yaml
deleted file mode 100644
index 3a9045a5..00000000
--- a/pydis_site/apps/home/resources/books/automate_the_boring_stuff.yaml
+++ /dev/null
@@ -1,16 +0,0 @@
-description: One of the best books out there for Python beginners. This book will
- teach you the basics of Python, while also teaching invaluable automation tools
- and techniques for solving common problems. You'll learn how to go about scraping
- 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.
-urls:
-- icon: regular/link
- title: E-book
- url: https://automatetheboringstuff.com/
-- icon: branding/amazon
- title: Amazon
- url: https://www.amazon.com/Automate-Boring-Stuff-Python-Programming/dp/1593275994/
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/books/effective_python.yaml b/pydis_site/apps/home/resources/books/effective_python.yaml
deleted file mode 100644
index 7f9d0dea..00000000
--- a/pydis_site/apps/home/resources/books/effective_python.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-description: A book that gives 90 best practices for writing excellent Python. Great
- for intermediates.
-name: Effective Python
-payment: paid
-urls:
-- icon: regular/link
- title: Website
- url: https://effectivepython.com/
-- icon: branding/amazon
- title: Amazon
- url: https://www.amazon.com/Effective-Python-Specific-Software-Development/dp/0134853989
-- icon: branding/github
- title: GitHub
- url: https://github.com/bslatkin/effectivepython
diff --git a/pydis_site/apps/home/resources/books/flask_web_development.yaml b/pydis_site/apps/home/resources/books/flask_web_development.yaml
deleted file mode 100644
index 613e0e50..00000000
--- a/pydis_site/apps/home/resources/books/flask_web_development.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-description: A comprehensive Flask walkthrough that has you building a complete social
- blogging application from scratch.
-name: Flask Web Development
-payment: paid
-urls:
-- icon: regular/link
- title: Website
- url: http://shop.oreilly.com/product/0636920031116.do
-- icon: branding/amazon
- title: Amazon
- url: https://www.amazon.com/Flask-Web-Development-Developing-Applications/dp/1449372627
-- icon: branding/github
- title: GitHub
- url: https://github.com/miguelgrinberg/flasky
diff --git a/pydis_site/apps/home/resources/books/fluent_python.yaml b/pydis_site/apps/home/resources/books/fluent_python.yaml
deleted file mode 100644
index ebfd5f91..00000000
--- a/pydis_site/apps/home/resources/books/fluent_python.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-description: A veritable tome of intermediate and advanced Python information. A must-read
- for any Python professional.
-name: Fluent Python
-payment: paid
-urls:
-- icon: regular/link
- title: Website
- url: https://www.oreilly.com/library/view/fluent-python/9781491946237/
-- icon: branding/amazon
- title: Amazon
- url: https://www.amazon.com/Fluent-Python-Concise-Effective-Programming/dp/1491946008
-- icon: branding/github
- title: GitHub
- url: https://github.com/fluentpython
diff --git a/pydis_site/apps/home/resources/books/mission_python.yaml b/pydis_site/apps/home/resources/books/mission_python.yaml
deleted file mode 100644
index 8cd91979..00000000
--- a/pydis_site/apps/home/resources/books/mission_python.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-description: Learn programming and Python while building a complete and awesome space-themed
- game using cutting-edge Python 3.6 and Pygame Zero. Extensive use of code examples,
- images, and walk-throughs make this a pleasure to both read and follow along. Excellent
- book for beginners.
-name: Mission Python
-payment: paid
-urls:
-- icon: regular/link
- title: Website
- url: https://www.sean.co.uk/books/mission-python/index.shtm
-- icon: branding/amazon
- title: Amazon
- url: https://www.amazon.com/Mission-Python-Code-Space-Adventure/dp/1593278578
diff --git a/pydis_site/apps/home/resources/books/python_cookbook.yaml b/pydis_site/apps/home/resources/books/python_cookbook.yaml
deleted file mode 100644
index 9fab8e48..00000000
--- a/pydis_site/apps/home/resources/books/python_cookbook.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-description: Complete with 'recipes' for various Python topics, including moving from
- Python 2 to Python 3.3
-name: Python Cookbook
-payment: paid
-urls:
-- icon: regular/link
- title: Website
- url: http://shop.oreilly.com/product/0636920027072.do
-- icon: branding/amazon
- title: Amazon
- url: https://www.amazon.com/Python-Cookbook-Third-David-Beazley/dp/1449340377
-- icon: branding/github
- title: GitHub
- url: https://github.com/dabeaz/python-cookbook
diff --git a/pydis_site/apps/home/resources/books/python_tricks.yaml b/pydis_site/apps/home/resources/books/python_tricks.yaml
deleted file mode 100644
index 0638058c..00000000
--- a/pydis_site/apps/home/resources/books/python_tricks.yaml
+++ /dev/null
@@ -1,12 +0,0 @@
-description: Full of useful Python tips, tricks and features. Get this if you have
- 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
-urls:
-- icon: regular/link
- title: Website
- url: https://realpython.com/products/python-tricks-book/
-- icon: branding/amazon
- title: Amazon
- url: https://www.amazon.com/Python-Tricks-Buffet-Awesome-Features/dp/1775093301
diff --git a/pydis_site/apps/home/resources/books/two_scoops_of_django.yaml b/pydis_site/apps/home/resources/books/two_scoops_of_django.yaml
deleted file mode 100644
index 85cfa0fc..00000000
--- a/pydis_site/apps/home/resources/books/two_scoops_of_django.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-description: This book is chock-full of material that will help you with your Django
- projects.
-name: Two Scoops of Django
-payment: paid
-urls:
-- icon: regular/link
- title: Website
- url: https://twoscoopspress.com/products/two-scoops-of-django-1-11
-- icon: branding/amazon
- title: Amazon
- url: https://www.amazon.com/Two-Scoops-Django-Best-Practices/dp/0981467342
-- icon: branding/github
- title: GitHub
- url: https://github.com/twoscoops/two-scoops-of-django-2.0-code-examples
diff --git a/pydis_site/apps/home/resources/communities/_category_info.yaml b/pydis_site/apps/home/resources/communities/_category_info.yaml
deleted file mode 100644
index eccb8b80..00000000
--- a/pydis_site/apps/home/resources/communities/_category_info.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
-description: Partnered communities that share part of our mission
-name: Communities
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/_category_info.yaml b/pydis_site/apps/home/resources/editors/_category_info.yaml
deleted file mode 100644
index f8dc1413..00000000
--- a/pydis_site/apps/home/resources/editors/_category_info.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
-description: Lightweight code editors supporting Python
-name: Editors
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/code_combat.yaml b/pydis_site/apps/home/resources/interactive_learning_tools/code_combat.yaml
deleted file mode 100644
index 39c25f0d..00000000
--- a/pydis_site/apps/home/resources/interactive_learning_tools/code_combat.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-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
-urls:
-- icon: regular/link
- title: Website
- url: https://codecombat.com/
-- icon: branding/github
- title: GitHub
- url: https://github.com/codecombat/codecombat
diff --git a/pydis_site/apps/home/resources/interactive_learning_tools/exercism.yaml b/pydis_site/apps/home/resources/interactive_learning_tools/exercism.yaml
deleted file mode 100644
index 3adb4138..00000000
--- a/pydis_site/apps/home/resources/interactive_learning_tools/exercism.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-description: Level up your programming skills with more than 2600 exercises across
- 47 programming languages, Python included. The website provides a mentored mode,
- 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
-urls:
-- icon: regular/link
- title: Website
- url: https://exercism.io/
-- icon: branding/github
- title: GitHub
- url: https://github.com/exercism/python
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/interactive_learning_tools/python_morsels.yaml b/pydis_site/apps/home/resources/interactive_learning_tools/python_morsels.yaml
deleted file mode 100644
index f883f8b7..00000000
--- a/pydis_site/apps/home/resources/interactive_learning_tools/python_morsels.yaml
+++ /dev/null
@@ -1,15 +0,0 @@
-description: 'Learn to write more idiomatic Python code with deliberate practice!
-
-
- Sign up for this service and receive one short Python exercise every week. After
- you attempt to work through the exercise, you''ll receive a number of solutions
- 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/
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/podcasts/podcast_dunder_init.yaml b/pydis_site/apps/home/resources/podcasts/podcast_dunder_init.yaml
deleted file mode 100644
index 8f0cac8b..00000000
--- a/pydis_site/apps/home/resources/podcasts/podcast_dunder_init.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
-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/
diff --git a/pydis_site/apps/home/resources/podcasts/python_bytes.yaml b/pydis_site/apps/home/resources/podcasts/python_bytes.yaml
deleted file mode 100644
index a3368d23..00000000
--- a/pydis_site/apps/home/resources/podcasts/python_bytes.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
-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/
diff --git a/pydis_site/apps/home/resources/podcasts/talk_python_to_me.yaml b/pydis_site/apps/home/resources/podcasts/talk_python_to_me.yaml
deleted file mode 100644
index 5ed101c4..00000000
--- a/pydis_site/apps/home/resources/podcasts/talk_python_to_me.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
-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/
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"))