diff options
Diffstat (limited to 'pydis_site/apps/home')
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")) |