aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/CODEOWNERS13
-rw-r--r--poetry.lock367
-rw-r--r--pydis_site/apps/api/__init__.py1
-rw-r--r--pydis_site/apps/api/apps.py10
-rw-r--r--pydis_site/apps/api/migrations/0059_populate_filterlists.py58
-rw-r--r--pydis_site/apps/api/migrations/0070_auto_20210519_0545.py23
-rw-r--r--pydis_site/apps/api/migrations/0072_merge_20210724_1354.py14
-rw-r--r--pydis_site/apps/api/migrations/0074_merge_20211105_0518.py14
-rw-r--r--pydis_site/apps/api/migrations/0074_voice_mute.py36
-rw-r--r--pydis_site/apps/api/migrations/0075_add_redirects_filter.py18
-rw-r--r--pydis_site/apps/api/migrations/0075_infraction_dm_sent.py18
-rw-r--r--pydis_site/apps/api/migrations/0076_merge_20211125_1941.py14
-rw-r--r--pydis_site/apps/api/migrations/0077_use_generic_jsonfield.py25
-rw-r--r--pydis_site/apps/api/migrations/0078_merge_20211213_0552.py14
-rw-r--r--pydis_site/apps/api/migrations/0079_merge_20220125_2022.py14
-rw-r--r--pydis_site/apps/api/models/bot/bot_setting.py3
-rw-r--r--pydis_site/apps/api/models/bot/filter_list.py1
-rw-r--r--pydis_site/apps/api/models/bot/infraction.py7
-rw-r--r--pydis_site/apps/api/models/bot/message.py2
-rw-r--r--pydis_site/apps/api/models/bot/metricity.py16
-rw-r--r--pydis_site/apps/api/models/bot/off_topic_channel_name.py7
-rw-r--r--pydis_site/apps/api/models/utils.py3
-rw-r--r--pydis_site/apps/api/serializers.py42
-rw-r--r--pydis_site/apps/api/signals.py12
-rw-r--r--pydis_site/apps/api/tests/test_off_topic_channel_names.py82
-rw-r--r--pydis_site/apps/api/tests/test_roles.py26
-rw-r--r--pydis_site/apps/api/tests/test_users.py77
-rw-r--r--pydis_site/apps/api/viewsets/bot/filter_list.py3
-rw-r--r--pydis_site/apps/api/viewsets/bot/infraction.py9
-rw-r--r--pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py27
-rw-r--r--pydis_site/apps/api/viewsets/bot/user.py21
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing.md6
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/bot.md27
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/setting-test-server-and-bot-account.md4
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot.md9
-rw-r--r--pydis_site/apps/content/resources/rules.md18
-rw-r--r--pydis_site/apps/content/urls.py2
-rw-r--r--pydis_site/apps/redirect/redirects.yaml49
-rw-r--r--pydis_site/apps/redirect/urls.py114
-rw-r--r--pydis_site/apps/resources/resources/adafruit.yaml (renamed from pydis_site/apps/resources/resources/communities/adafruit.yaml)23
-rw-r--r--pydis_site/apps/resources/resources/atom.yaml (renamed from pydis_site/apps/resources/resources/tools/editors/atom.yaml)11
-rw-r--r--pydis_site/apps/resources/resources/automate_the_boring_stuff_book.yaml (renamed from pydis_site/apps/resources/resources/reading/books/automate_the_boring_stuff.yaml)19
-rw-r--r--pydis_site/apps/resources/resources/automate_the_boring_stuff_course.yaml13
-rw-r--r--pydis_site/apps/resources/resources/awesome_programming_discord.yaml (renamed from pydis_site/apps/resources/resources/communities/awesome_programming_discord.yaml)11
-rw-r--r--pydis_site/apps/resources/resources/byte_of_python.yaml (renamed from pydis_site/apps/resources/resources/reading/books/byte_of_python.yaml)21
-rw-r--r--pydis_site/apps/resources/resources/code_combat.yaml (renamed from pydis_site/apps/resources/resources/interactive/code_combat.yaml)17
-rw-r--r--pydis_site/apps/resources/resources/communities/_category_info.yaml2
-rw-r--r--pydis_site/apps/resources/resources/corey_schafer.yaml (renamed from pydis_site/apps/resources/resources/videos/corey_schafer.yaml)21
-rw-r--r--pydis_site/apps/resources/resources/courses/_category_info.yaml4
-rw-r--r--pydis_site/apps/resources/resources/courses/automate_the_boring_stuff_with_python.yaml5
-rw-r--r--pydis_site/apps/resources/resources/data_science_from_scratch.yaml22
-rw-r--r--pydis_site/apps/resources/resources/edublocks.yaml (renamed from pydis_site/apps/resources/resources/interactive/edublocks.yaml)10
-rw-r--r--pydis_site/apps/resources/resources/effective_python.yaml21
-rw-r--r--pydis_site/apps/resources/resources/exercism.yaml (renamed from pydis_site/apps/resources/resources/interactive/exercism.yaml)17
-rw-r--r--pydis_site/apps/resources/resources/flask_web_development.yaml21
-rw-r--r--pydis_site/apps/resources/resources/fluent_python.yaml21
-rw-r--r--pydis_site/apps/resources/resources/getting_started_with_kivy.yaml (renamed from pydis_site/apps/resources/resources/reading/tutorials/getting_started_with_kivy.yaml)11
-rw-r--r--pydis_site/apps/resources/resources/getting_started_with_python_for_non_programmers.yaml (renamed from pydis_site/apps/resources/resources/reading/tutorials/getting_started_with_python_for_non_programmers.yaml)10
-rw-r--r--pydis_site/apps/resources/resources/getting_started_with_python_for_programmers.yaml (renamed from pydis_site/apps/resources/resources/reading/tutorials/getting_started_with_python_for_programmers.yaml)9
-rw-r--r--pydis_site/apps/resources/resources/google_colab.yaml17
-rw-r--r--pydis_site/apps/resources/resources/hitchhikers_guide_to_python.yaml18
-rw-r--r--pydis_site/apps/resources/resources/inferential_thinking.yaml (renamed from pydis_site/apps/resources/resources/reading/books/inferential_thinking.yaml)16
-rw-r--r--pydis_site/apps/resources/resources/interactive/_category_info.yaml4
-rw-r--r--pydis_site/apps/resources/resources/jetbrains_academy.yaml (renamed from pydis_site/apps/resources/resources/interactive/jetbrains_academy.yaml)12
-rw-r--r--pydis_site/apps/resources/resources/jetbrains_videos.yaml (renamed from pydis_site/apps/resources/resources/videos/jetbrains.yaml)19
-rw-r--r--pydis_site/apps/resources/resources/jim_shaped_coding.yaml (renamed from pydis_site/apps/resources/resources/videos/jim_shaped_coding.yaml)19
-rw-r--r--pydis_site/apps/resources/resources/kaggle_pandas_tutorial.yaml13
-rw-r--r--pydis_site/apps/resources/resources/kivy.yaml (renamed from pydis_site/apps/resources/resources/communities/kivy.yaml)19
-rw-r--r--pydis_site/apps/resources/resources/microsoft.yaml (renamed from pydis_site/apps/resources/resources/communities/microsoft.yaml)18
-rw-r--r--pydis_site/apps/resources/resources/microsoft_videos.yaml (renamed from pydis_site/apps/resources/resources/videos/microsoft.yaml)18
-rw-r--r--pydis_site/apps/resources/resources/mission_python.yaml (renamed from pydis_site/apps/resources/resources/reading/books/mission_python.yaml)21
-rw-r--r--pydis_site/apps/resources/resources/mit_introduction_to_computer_science_and_programming.yaml (renamed from pydis_site/apps/resources/resources/courses/mit_introduction_to_computer_science_and_programming.yaml)12
-rw-r--r--pydis_site/apps/resources/resources/mu_editor.yaml (renamed from pydis_site/apps/resources/resources/tools/editors/mu_editor.yaml)10
-rw-r--r--pydis_site/apps/resources/resources/netbats_project_ideas.yaml14
-rw-r--r--pydis_site/apps/resources/resources/neural_networks_from_scratch_in_python.yaml (renamed from pydis_site/apps/resources/resources/reading/books/neural_networks_from_scratch_in_python.yaml)17
-rw-r--r--pydis_site/apps/resources/resources/pallets.yaml (renamed from pydis_site/apps/resources/resources/communities/pallets.yaml)17
-rw-r--r--pydis_site/apps/resources/resources/panda3d.yaml (renamed from pydis_site/apps/resources/resources/communities/panda3d.yaml)18
-rw-r--r--pydis_site/apps/resources/resources/people_postgres_data.yaml (renamed from pydis_site/apps/resources/resources/communities/people_postgres_data.yaml)18
-rw-r--r--pydis_site/apps/resources/resources/podcast_dunder_init.yaml (renamed from pydis_site/apps/resources/resources/podcasts/podcast_dunder_init.yaml)11
-rw-r--r--pydis_site/apps/resources/resources/podcasts/_category_info.yaml4
-rw-r--r--pydis_site/apps/resources/resources/practical_python_programming.yaml (renamed from pydis_site/apps/resources/resources/courses/practical_python_programming.yaml)9
-rw-r--r--pydis_site/apps/resources/resources/pycharm.yaml (renamed from pydis_site/apps/resources/resources/tools/ides/pycharm.yaml)11
-rw-r--r--pydis_site/apps/resources/resources/pyglet.yaml (renamed from pydis_site/apps/resources/resources/communities/pyglet.yaml)18
-rw-r--r--pydis_site/apps/resources/resources/python_bytes.yaml (renamed from pydis_site/apps/resources/resources/podcasts/python_bytes.yaml)10
-rw-r--r--pydis_site/apps/resources/resources/python_cheat_sheet.yaml (renamed from pydis_site/apps/resources/resources/reading/tutorials/python_cheat_sheet.yaml)10
-rw-r--r--pydis_site/apps/resources/resources/python_cookbook.yaml21
-rw-r--r--pydis_site/apps/resources/resources/python_crash_course.yaml (renamed from pydis_site/apps/resources/resources/reading/books/python_crash_course.yaml)21
-rw-r--r--pydis_site/apps/resources/resources/python_developer_guide.yaml (renamed from pydis_site/apps/resources/resources/reading/tutorials/python_developer_guide.yaml)10
-rw-r--r--pydis_site/apps/resources/resources/python_discord_videos.yaml16
-rw-r--r--pydis_site/apps/resources/resources/python_morsels.yaml (renamed from pydis_site/apps/resources/resources/interactive/python_morsels.yaml)11
-rw-r--r--pydis_site/apps/resources/resources/python_subreddit.yaml (renamed from pydis_site/apps/resources/resources/communities/subreddit.yaml)10
-rw-r--r--pydis_site/apps/resources/resources/python_tricks.yaml19
-rw-r--r--pydis_site/apps/resources/resources/python_tutor.yaml (renamed from pydis_site/apps/resources/resources/interactive/python_tutor.yaml)12
-rw-r--r--pydis_site/apps/resources/resources/reading/_category_info.yaml2
-rw-r--r--pydis_site/apps/resources/resources/reading/books/_category_info.yaml5
-rw-r--r--pydis_site/apps/resources/resources/reading/books/effective_python.yaml15
-rw-r--r--pydis_site/apps/resources/resources/reading/books/flask_web_development.yaml14
-rw-r--r--pydis_site/apps/resources/resources/reading/books/fluent_python.yaml14
-rw-r--r--pydis_site/apps/resources/resources/reading/books/hitchhikers_guide_to_python.yaml11
-rw-r--r--pydis_site/apps/resources/resources/reading/books/python_cookbook.yaml14
-rw-r--r--pydis_site/apps/resources/resources/reading/books/python_tricks.yaml12
-rw-r--r--pydis_site/apps/resources/resources/reading/books/two_scoops_of_django.yaml14
-rw-r--r--pydis_site/apps/resources/resources/reading/tutorials/_category_info.yaml5
-rw-r--r--pydis_site/apps/resources/resources/real_python.yaml (renamed from pydis_site/apps/resources/resources/communities/real_python.yaml)16
-rw-r--r--pydis_site/apps/resources/resources/regex101.yaml15
-rw-r--r--pydis_site/apps/resources/resources/repl_it.yaml14
-rw-r--r--pydis_site/apps/resources/resources/screen_readers.yaml (renamed from pydis_site/apps/resources/resources/tools/accessibility_tools/screen_readers.yaml)12
-rw-r--r--pydis_site/apps/resources/resources/sentdex.yaml (renamed from pydis_site/apps/resources/resources/videos/sentdex.yaml)25
-rw-r--r--pydis_site/apps/resources/resources/simple_guide_to_git.yaml (renamed from pydis_site/apps/resources/resources/reading/tutorials/simple_guide_to_git.yaml)10
-rw-r--r--pydis_site/apps/resources/resources/socratica.yaml23
-rw-r--r--pydis_site/apps/resources/resources/sololearn.yaml (renamed from pydis_site/apps/resources/resources/interactive/sololearn.yaml)12
-rw-r--r--pydis_site/apps/resources/resources/spyder.yaml (renamed from pydis_site/apps/resources/resources/tools/ides/spyder.yaml)11
-rw-r--r--pydis_site/apps/resources/resources/sublime_text.yaml (renamed from pydis_site/apps/resources/resources/tools/editors/sublime_text.yaml)11
-rw-r--r--pydis_site/apps/resources/resources/talk_python_to_me.yaml (renamed from pydis_site/apps/resources/resources/podcasts/talk_python_to_me.yaml)11
-rw-r--r--pydis_site/apps/resources/resources/talon_voice.yaml (renamed from pydis_site/apps/resources/resources/tools/accessibility_tools/talon_voice.yaml)11
-rw-r--r--pydis_site/apps/resources/resources/test_and_code.yaml (renamed from pydis_site/apps/resources/resources/podcasts/test_and_code.yaml)12
-rw-r--r--pydis_site/apps/resources/resources/the_flask_mega_tutorial.yaml (renamed from pydis_site/apps/resources/resources/reading/tutorials/the_flask_mega_tutorial.yaml)11
-rw-r--r--pydis_site/apps/resources/resources/the_real_python_podcast.yaml (renamed from pydis_site/apps/resources/resources/podcasts/the_real_python_podcast.yaml)11
-rw-r--r--pydis_site/apps/resources/resources/think_python.yaml (renamed from pydis_site/apps/resources/resources/reading/books/think_python.yaml)21
-rw-r--r--pydis_site/apps/resources/resources/thonny.yaml (renamed from pydis_site/apps/resources/resources/tools/ides/thonny.yaml)9
-rw-r--r--pydis_site/apps/resources/resources/tools/_category_info.yaml4
-rw-r--r--pydis_site/apps/resources/resources/tools/accessibility_tools/_category_info.yaml5
-rw-r--r--pydis_site/apps/resources/resources/tools/editors/_category_info.yaml5
-rw-r--r--pydis_site/apps/resources/resources/tools/editors/google_collab.yaml7
-rw-r--r--pydis_site/apps/resources/resources/tools/ides/_category_info.yaml5
-rw-r--r--pydis_site/apps/resources/resources/tools/ides/replit.yaml5
-rw-r--r--pydis_site/apps/resources/resources/two_scoops_of_django.yaml20
-rw-r--r--pydis_site/apps/resources/resources/university_of_michigan.yaml (renamed from pydis_site/apps/resources/resources/courses/university_of_michigan.yaml)10
-rw-r--r--pydis_site/apps/resources/resources/university_of_toronto.yaml (renamed from pydis_site/apps/resources/resources/courses/university_of_toronto.yaml)11
-rw-r--r--pydis_site/apps/resources/resources/vcokltfre_discord_bot_tutorial.yaml14
-rw-r--r--pydis_site/apps/resources/resources/videos/_category_info.yaml2
-rw-r--r--pydis_site/apps/resources/resources/videos/python_discord.yaml8
-rw-r--r--pydis_site/apps/resources/resources/visual_studio_code.yaml (renamed from pydis_site/apps/resources/resources/tools/editors/visual_studio_code.yaml)11
-rw-r--r--pydis_site/apps/resources/resources/wtf_python.yaml (renamed from pydis_site/apps/resources/resources/reading/tutorials/wtf_python.yaml)10
-rw-r--r--pydis_site/apps/resources/templatetags/get_category_icon.py39
-rw-r--r--pydis_site/apps/resources/templatetags/to_kebabcase.py39
-rw-r--r--pydis_site/apps/resources/tests/test_to_kebabcase.py19
-rw-r--r--pydis_site/apps/resources/tests/test_views.py17
-rw-r--r--pydis_site/apps/resources/tests/testing_resources/testing/_category_info.yaml1
-rw-r--r--pydis_site/apps/resources/tests/testing_resources/testing/foobar/_category_info.yaml1
-rw-r--r--pydis_site/apps/resources/tests/testing_resources/testing/foobar/resource_test.yaml1
-rw-r--r--pydis_site/apps/resources/tests/testing_resources/testing/my_resource.yaml1
-rw-r--r--pydis_site/apps/resources/urls.py20
-rw-r--r--pydis_site/apps/resources/utils.py42
-rw-r--r--pydis_site/apps/resources/views/__init__.py5
-rw-r--r--pydis_site/apps/resources/views/resources.py127
-rw-r--r--pydis_site/apps/resources/views/resources_list.py39
-rw-r--r--pydis_site/settings.py1
-rw-r--r--pydis_site/static/css/base/base.css15
-rw-r--r--pydis_site/static/css/collapsibles.css11
-rw-r--r--pydis_site/static/css/content/page.css13
-rw-r--r--pydis_site/static/css/resources/resources.css298
-rw-r--r--pydis_site/static/css/resources/resources_list.css55
-rw-r--r--pydis_site/static/images/resources/duck_pond_404.jpgbin0 -> 123824 bytes
-rw-r--r--pydis_site/static/images/sponsors/netcup.pngbin0 -> 9492 bytes
-rw-r--r--pydis_site/static/js/collapsibles.js67
-rw-r--r--pydis_site/static/js/content/page.js13
-rw-r--r--pydis_site/static/js/fuzzysort/LICENSE.md21
-rw-r--r--pydis_site/static/js/fuzzysort/fuzzysort.js636
-rw-r--r--pydis_site/static/js/resources/resources.js367
-rw-r--r--pydis_site/templates/base/base.html5
-rw-r--r--pydis_site/templates/base/footer.html2
-rw-r--r--pydis_site/templates/base/navbar.html6
-rw-r--r--pydis_site/templates/content/base.html3
-rw-r--r--pydis_site/templates/content/dropdown.html2
-rw-r--r--pydis_site/templates/home/index.html3
-rw-r--r--pydis_site/templates/resources/resource_box.html77
-rw-r--r--pydis_site/templates/resources/resource_box_header.html3
-rw-r--r--pydis_site/templates/resources/resources.html240
-rw-r--r--pydis_site/templates/resources/resources_list.html52
-rw-r--r--pydis_site/urls.py16
-rw-r--r--pyproject.toml13
-rw-r--r--static-builds/README.md14
173 files changed, 3691 insertions, 1039 deletions
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index dc4b1a92..b6004466 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -6,17 +6,12 @@ pydis_site/apps/api/viewsets/bot/infraction.py @MarkKoz
pydis_site/apps/home/** @ks129
# Django ORM
-**/migrations/** @Akarys42
-**/models/** @Akarys42 @Den4200
+**/models/** @Den4200
# CI & Docker
-.github/workflows/** @MarkKoz @Akarys42 @SebastiaanZ @Den4200 @ks129
-Dockerfile @MarkKoz @Akarys42 @Den4200
-docker-compose.yml @MarkKoz @Akarys42 @Den4200
-
-# Tools
-poetry.lock @Akarys42
-pyproject.toml @Akarys42
+.github/workflows/** @MarkKoz @SebastiaanZ @Den4200 @ks129
+Dockerfile @MarkKoz @Den4200
+docker-compose.yml @MarkKoz @Den4200
# Metricity
pydis_site/apps/api/models/bot/metricity.py @jb3
diff --git a/poetry.lock b/poetry.lock
index eac58fdb..3b26c275 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,58 +1,50 @@
[[package]]
name = "asgiref"
-version = "3.4.1"
+version = "3.5.0"
description = "ASGI specs, helper code, and adapters"
category = "main"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
[package.extras]
tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"]
[[package]]
name = "attrs"
-version = "21.2.0"
+version = "21.4.0"
description = "Classes Without Boilerplate"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.extras]
-dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"]
+dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
-tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"]
-tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"]
-
-[[package]]
-name = "backports.entry-points-selectable"
-version = "1.1.0"
-description = "Compatibility shim providing selectable entry points for older implementations"
-category = "dev"
-optional = false
-python-versions = ">=2.7"
-
-[package.extras]
-docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
-testing = ["pytest (>=4.6)", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"]
+tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
+tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]
[[package]]
name = "bandit"
-version = "1.7.0"
+version = "1.7.2"
description = "Security oriented static analyser for python code."
category = "dev"
optional = false
-python-versions = ">=3.5"
+python-versions = ">=3.7"
[package.dependencies]
colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""}
GitPython = ">=1.0.1"
PyYAML = ">=5.3.1"
-six = ">=1.10.0"
stevedore = ">=1.20.0"
+[package.extras]
+test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)", "toml"]
+toml = ["toml"]
+yaml = ["pyyaml"]
+
[[package]]
name = "certifi"
-version = "2021.5.30"
+version = "2021.10.8"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
@@ -68,7 +60,7 @@ python-versions = ">=3.6.1"
[[package]]
name = "charset-normalizer"
-version = "2.0.6"
+version = "2.0.11"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "main"
optional = false
@@ -114,7 +106,7 @@ yaml = ["PyYAML (>=3.10)"]
[[package]]
name = "distlib"
-version = "0.3.3"
+version = "0.3.4"
description = "Distribution utilities"
category = "dev"
optional = false
@@ -122,14 +114,14 @@ python-versions = "*"
[[package]]
name = "django"
-version = "3.0.14"
+version = "3.1.14"
description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design."
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
-asgiref = ">=3.2,<4.0"
+asgiref = ">=3.2.10,<4"
pytz = "*"
sqlparse = ">=0.2.2"
@@ -139,7 +131,7 @@ bcrypt = ["bcrypt"]
[[package]]
name = "django-distill"
-version = "2.9.0"
+version = "2.9.2"
description = "Static site renderer and publisher for Django."
category = "main"
optional = false
@@ -159,18 +151,18 @@ python-versions = "*"
[[package]]
name = "django-filter"
-version = "2.1.0"
+version = "21.1"
description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically."
category = "main"
optional = false
-python-versions = ">=3.4"
+python-versions = ">=3.6"
[package.dependencies]
-Django = ">=1.11"
+Django = ">=2.2"
[[package]]
name = "django-prometheus"
-version = "2.1.0"
+version = "2.2.0"
description = "Django middlewares to monitor your application with Prometheus.io."
category = "main"
optional = false
@@ -196,14 +188,14 @@ dev = ["flake8 (>=3.8,<4.0)", "flake8-annotations (>=2.0,<3.0)", "flake8-bugbear
[[package]]
name = "djangorestframework"
-version = "3.11.2"
+version = "3.12.4"
description = "Web APIs for Django, made easy."
category = "main"
optional = false
python-versions = ">=3.5"
[package.dependencies]
-django = ">=1.11"
+django = ">=2.2"
[[package]]
name = "docopt"
@@ -215,11 +207,11 @@ python-versions = "*"
[[package]]
name = "filelock"
-version = "3.3.0"
+version = "3.4.2"
description = "A platform independent file lock."
category = "dev"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
[package.extras]
docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"]
@@ -240,14 +232,14 @@ pyflakes = ">=2.3.0,<2.4.0"
[[package]]
name = "flake8-annotations"
-version = "2.6.2"
+version = "2.7.0"
description = "Flake8 Type Annotation Checks"
category = "dev"
optional = false
-python-versions = ">=3.6.1,<4.0.0"
+python-versions = ">=3.6.2,<4.0.0"
[package.dependencies]
-flake8 = ">=3.7,<4.0"
+flake8 = ">=3.7,<5.0"
[[package]]
name = "flake8-bandit"
@@ -325,14 +317,14 @@ flake8 = "*"
[[package]]
name = "flake8-tidy-imports"
-version = "4.4.1"
+version = "4.6.0"
description = "A flake8 plugin that helps you write tidier imports."
category = "dev"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
[package.dependencies]
-flake8 = ">=3.8.0,<4"
+flake8 = ">=3.8.0"
[[package]]
name = "flake8-todo"
@@ -347,18 +339,18 @@ pycodestyle = ">=2.0.0,<3.0.0"
[[package]]
name = "gitdb"
-version = "4.0.7"
+version = "4.0.9"
description = "Git Object Database"
category = "dev"
optional = false
-python-versions = ">=3.4"
+python-versions = ">=3.6"
[package.dependencies]
-smmap = ">=3.0.1,<5"
+smmap = ">=3.0.1,<6"
[[package]]
name = "gitpython"
-version = "3.1.24"
+version = "3.1.26"
description = "GitPython is a python library used to interact with Git repositories"
category = "dev"
optional = false
@@ -366,7 +358,6 @@ python-versions = ">=3.7"
[package.dependencies]
gitdb = ">=4.0.1,<5"
-typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.10\""}
[[package]]
name = "gunicorn"
@@ -384,24 +375,40 @@ tornado = ["tornado (>=0.2)"]
[[package]]
name = "identify"
-version = "2.3.0"
+version = "2.4.6"
description = "File identification library for Python"
category = "dev"
optional = false
-python-versions = ">=3.6.1"
+python-versions = ">=3.7"
[package.extras]
-license = ["editdistance-s"]
+license = ["ukkonen"]
[[package]]
name = "idna"
-version = "3.2"
+version = "3.3"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
python-versions = ">=3.5"
[[package]]
+name = "importlib-metadata"
+version = "4.10.1"
+description = "Read metadata from Python packages"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+zipp = ">=0.5"
+
+[package.extras]
+docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
+perf = ["ipython"]
+testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
+
+[[package]]
name = "libsass"
version = "0.21.0"
description = "Sass for Python: A straightforward binding of libsass for Python."
@@ -414,12 +421,15 @@ six = "*"
[[package]]
name = "markdown"
-version = "3.3.4"
+version = "3.3.6"
description = "Python implementation of Markdown."
category = "main"
optional = false
python-versions = ">=3.6"
+[package.dependencies]
+importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""}
+
[package.extras]
testing = ["coverage", "pyyaml"]
@@ -449,7 +459,7 @@ python-versions = "*"
[[package]]
name = "pbr"
-version = "5.6.0"
+version = "5.8.0"
description = "Python Build Reasonableness"
category = "dev"
optional = false
@@ -469,11 +479,11 @@ flake8-polyfill = ">=1.0.2,<2"
[[package]]
name = "platformdirs"
-version = "2.4.0"
+version = "2.4.1"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
[package.extras]
docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"]
@@ -481,7 +491,7 @@ test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock
[[package]]
name = "pre-commit"
-version = "2.15.0"
+version = "2.17.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
category = "dev"
optional = false
@@ -497,18 +507,18 @@ virtualenv = ">=20.0.8"
[[package]]
name = "prometheus-client"
-version = "0.11.0"
+version = "0.13.1"
description = "Python client for the Prometheus monitoring system."
category = "main"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+python-versions = ">=3.6"
[package.extras]
twisted = ["twisted"]
[[package]]
name = "psutil"
-version = "5.8.0"
+version = "5.9.0"
description = "Cross-platform lib for process and system monitoring in Python."
category = "dev"
optional = false
@@ -549,11 +559,11 @@ toml = ["toml"]
[[package]]
name = "pyfakefs"
-version = "4.4.0"
+version = "4.5.4"
description = "pyfakefs implements a fake file system that mocks the Python file system modules."
category = "dev"
optional = false
-python-versions = ">=3.5"
+python-versions = ">=3.6"
[[package]]
name = "pyflakes"
@@ -607,7 +617,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
[[package]]
name = "requests"
-version = "2.26.0"
+version = "2.27.1"
description = "Python HTTP for Humans."
category = "main"
optional = false
@@ -661,15 +671,15 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "smmap"
-version = "4.0.0"
+version = "5.0.0"
description = "A pure Python implementation of a sliding window memory map manager"
category = "dev"
optional = false
-python-versions = ">=3.5"
+python-versions = ">=3.6"
[[package]]
name = "snowballstemmer"
-version = "2.1.0"
+version = "2.2.0"
description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms."
category = "dev"
optional = false
@@ -685,7 +695,7 @@ python-versions = ">=3.5"
[[package]]
name = "stevedore"
-version = "3.4.0"
+version = "3.5.0"
description = "Manage dynamic plugins for Python applications"
category = "dev"
optional = false
@@ -716,16 +726,8 @@ optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
-name = "typing-extensions"
-version = "3.10.0.2"
-description = "Backported and Experimental Type Hints for Python 3.5+"
-category = "dev"
-optional = false
-python-versions = "*"
-
-[[package]]
name = "urllib3"
-version = "1.26.7"
+version = "1.26.8"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main"
optional = false
@@ -738,21 +740,20 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]]
name = "virtualenv"
-version = "20.8.1"
+version = "20.13.0"
description = "Virtual Python Environment builder"
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[package.dependencies]
-"backports.entry-points-selectable" = ">=1.0.4"
distlib = ">=0.3.1,<1"
-filelock = ">=3.0.0,<4"
+filelock = ">=3.2,<4"
platformdirs = ">=2,<3"
six = ">=1.9.0,<2"
[package.extras]
-docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"]
+docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"]
testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"]
[[package]]
@@ -766,39 +767,47 @@ python-versions = ">=3.5, <4"
[package.extras]
brotli = ["brotli"]
+[[package]]
+name = "zipp"
+version = "3.7.0"
+description = "Backport of pathlib-compatible object wrapper for zip files"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
+testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
+
[metadata]
lock-version = "1.1"
python-versions = "3.9.*"
-content-hash = "9f0c069c14e2dbff63d58474702693f0c02b8cfd30e5af38303975a73b71bcfd"
+content-hash = "fc9b20c33c65a289122d710844285ac20d7598e65c7f8237f8903509f5b2dea4"
[metadata.files]
asgiref = [
- {file = "asgiref-3.4.1-py3-none-any.whl", hash = "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214"},
- {file = "asgiref-3.4.1.tar.gz", hash = "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9"},
+ {file = "asgiref-3.5.0-py3-none-any.whl", hash = "sha256:88d59c13d634dcffe0510be048210188edd79aeccb6a6c9028cdad6f31d730a9"},
+ {file = "asgiref-3.5.0.tar.gz", hash = "sha256:2f8abc20f7248433085eda803936d98992f1343ddb022065779f37c5da0181d0"},
]
attrs = [
- {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"},
- {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
-]
-"backports.entry-points-selectable" = [
- {file = "backports.entry_points_selectable-1.1.0-py2.py3-none-any.whl", hash = "sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc"},
- {file = "backports.entry_points_selectable-1.1.0.tar.gz", hash = "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a"},
+ {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
+ {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
]
bandit = [
- {file = "bandit-1.7.0-py3-none-any.whl", hash = "sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07"},
- {file = "bandit-1.7.0.tar.gz", hash = "sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608"},
+ {file = "bandit-1.7.2-py3-none-any.whl", hash = "sha256:e20402cadfd126d85b68ed4c8862959663c8c372dbbb1fca8f8e2c9f55a067ec"},
+ {file = "bandit-1.7.2.tar.gz", hash = "sha256:6d11adea0214a43813887bfe71a377b5a9955e4c826c8ffd341b494e3ab25260"},
]
certifi = [
- {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"},
- {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"},
+ {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"},
+ {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"},
]
cfgv = [
{file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
{file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
]
charset-normalizer = [
- {file = "charset-normalizer-2.0.6.tar.gz", hash = "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f"},
- {file = "charset_normalizer-2.0.6-py3-none-any.whl", hash = "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6"},
+ {file = "charset-normalizer-2.0.11.tar.gz", hash = "sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c"},
+ {file = "charset_normalizer-2.0.11-py3-none-any.whl", hash = "sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45"},
]
colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
@@ -863,50 +872,50 @@ coveralls = [
{file = "coveralls-2.2.0.tar.gz", hash = "sha256:b990ba1f7bc4288e63340be0433698c1efe8217f78c689d254c2540af3d38617"},
]
distlib = [
- {file = "distlib-0.3.3-py2.py3-none-any.whl", hash = "sha256:c8b54e8454e5bf6237cc84c20e8264c3e991e824ef27e8f1e81049867d861e31"},
- {file = "distlib-0.3.3.zip", hash = "sha256:d982d0751ff6eaaab5e2ec8e691d949ee80eddf01a62eaa96ddb11531fe16b05"},
+ {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"},
+ {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"},
]
django = [
- {file = "Django-3.0.14-py3-none-any.whl", hash = "sha256:9bc7aa619ed878fedba62ce139abe663a147dccfd20e907725ec11e02a1ca225"},
- {file = "Django-3.0.14.tar.gz", hash = "sha256:d58d8394036db75a81896037d757357e79406e8f68816c3e8a28721c1d9d4c11"},
+ {file = "Django-3.1.14-py3-none-any.whl", hash = "sha256:0fabc786489af16ad87a8c170ba9d42bfd23f7b699bd5ef05675864e8d012859"},
+ {file = "Django-3.1.14.tar.gz", hash = "sha256:72a4a5a136a214c39cf016ccdd6b69e2aa08c7479c66d93f3a9b5e4bb9d8a347"},
]
django-distill = [
- {file = "django-distill-2.9.0.tar.gz", hash = "sha256:08f31dcde2e79e73c0bc4f36941830603a811cc89472be11f79f14affb460d84"},
+ {file = "django-distill-2.9.2.tar.gz", hash = "sha256:91d5f45c2ff78b8efd4805ff5ec17df4ba815bbf51ca12a2cea65727d2f1d42e"},
]
django-environ = [
{file = "django-environ-0.4.5.tar.gz", hash = "sha256:6c9d87660142608f63ec7d5ce5564c49b603ea8ff25da595fd6098f6dc82afde"},
{file = "django_environ-0.4.5-py2.py3-none-any.whl", hash = "sha256:c57b3c11ec1f319d9474e3e5a79134f40174b17c7cc024bbb2fad84646b120c4"},
]
django-filter = [
- {file = "django-filter-2.1.0.tar.gz", hash = "sha256:3dafb7d2810790498895c22a1f31b2375795910680ac9c1432821cbedb1e176d"},
- {file = "django_filter-2.1.0-py3-none-any.whl", hash = "sha256:a3014de317bef0cd43075a0f08dfa1d319a7ccc5733c3901fb860da70b0dda68"},
+ {file = "django-filter-21.1.tar.gz", hash = "sha256:632a251fa8f1aadb4b8cceff932bb52fe2f826dd7dfe7f3eac40e5c463d6836e"},
+ {file = "django_filter-21.1-py3-none-any.whl", hash = "sha256:f4a6737a30104c98d2e2a5fb93043f36dd7978e0c7ddc92f5998e85433ea5063"},
]
django-prometheus = [
- {file = "django-prometheus-2.1.0.tar.gz", hash = "sha256:dd3f8da1399140fbef5c00d1526a23d1ade286b144281c325f8e409a781643f2"},
- {file = "django_prometheus-2.1.0-py2.py3-none-any.whl", hash = "sha256:c338d6efde1ca336e90c540b5e87afe9287d7bcc82d651a778f302b0be17a933"},
+ {file = "django-prometheus-2.2.0.tar.gz", hash = "sha256:240378a1307c408bd5fc85614a3a57f1ce633d4a222c9e291e2bbf325173b801"},
+ {file = "django_prometheus-2.2.0-py2.py3-none-any.whl", hash = "sha256:e6616770d8820b8834762764bf1b76ec08e1b98e72a6f359d488a2e15fe3537c"},
]
django-simple-bulma = [
{file = "django-simple-bulma-2.4.0.tar.gz", hash = "sha256:99a15261b0c61062a128af3c6a45da9c066d6a4a548c9063464e0fb7a5438aa1"},
{file = "django_simple_bulma-2.4.0-py3-none-any.whl", hash = "sha256:95d5e26bebbf6a0184e33df844a0ff534bdfd91431e413d1a844d47a75c55fff"},
]
djangorestframework = [
- {file = "djangorestframework-3.11.2-py3-none-any.whl", hash = "sha256:5cc724dc4b076463497837269107e1995b1fbc917468d1b92d188fd1af9ea789"},
- {file = "djangorestframework-3.11.2.tar.gz", hash = "sha256:a5967b68a04e0d97d10f4df228e30f5a2d82ba63b9d03e1759f84993b7bf1b53"},
+ {file = "djangorestframework-3.12.4-py3-none-any.whl", hash = "sha256:6d1d59f623a5ad0509fe0d6bfe93cbdfe17b8116ebc8eda86d45f6e16e819aaf"},
+ {file = "djangorestframework-3.12.4.tar.gz", hash = "sha256:f747949a8ddac876e879190df194b925c177cdeb725a099db1460872f7c0a7f2"},
]
docopt = [
{file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"},
]
filelock = [
- {file = "filelock-3.3.0-py3-none-any.whl", hash = "sha256:bbc6a0382fe8ec4744ecdf6683a2e07f65eb10ff1aff53fc02a202565446cde0"},
- {file = "filelock-3.3.0.tar.gz", hash = "sha256:8c7eab13dc442dc249e95158bcc12dec724465919bdc9831fdbf0660f03d1785"},
+ {file = "filelock-3.4.2-py3-none-any.whl", hash = "sha256:cf0fc6a2f8d26bd900f19bf33915ca70ba4dd8c56903eeb14e1e7a2fd7590146"},
+ {file = "filelock-3.4.2.tar.gz", hash = "sha256:38b4f4c989f9d06d44524df1b24bd19e167d851f19b50bf3e3559952dddc5b80"},
]
flake8 = [
{file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"},
{file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"},
]
flake8-annotations = [
- {file = "flake8-annotations-2.6.2.tar.gz", hash = "sha256:0d6cd2e770b5095f09689c9d84cc054c51b929c41a68969ea1beb4b825cac515"},
- {file = "flake8_annotations-2.6.2-py3-none-any.whl", hash = "sha256:d10c4638231f8a50c0a597c4efce42bd7b7d85df4f620a0ddaca526138936a4f"},
+ {file = "flake8-annotations-2.7.0.tar.gz", hash = "sha256:52e53c05b0c06cac1c2dec192ea2c36e85081238add3bd99421d56f574b9479b"},
+ {file = "flake8_annotations-2.7.0-py3-none-any.whl", hash = "sha256:3edfbbfb58e404868834fe6ec3eaf49c139f64f0701259f707d043185545151e"},
]
flake8-bandit = [
{file = "flake8_bandit-2.1.2.tar.gz", hash = "sha256:687fc8da2e4a239b206af2e54a90093572a60d0954f3054e23690739b0b0de3b"},
@@ -932,31 +941,35 @@ flake8-string-format = [
{file = "flake8_string_format-0.3.0-py2.py3-none-any.whl", hash = "sha256:812ff431f10576a74c89be4e85b8e075a705be39bc40c4b4278b5b13e2afa9af"},
]
flake8-tidy-imports = [
- {file = "flake8-tidy-imports-4.4.1.tar.gz", hash = "sha256:c18b3351b998787db071e766e318da1f0bd9d5cecc69c4022a69e7aa2efb2c51"},
- {file = "flake8_tidy_imports-4.4.1-py3-none-any.whl", hash = "sha256:631a1ba9daaedbe8bb53f6086c5a92b390e98371205259e0e311a378df8c3dc8"},
+ {file = "flake8-tidy-imports-4.6.0.tar.gz", hash = "sha256:3e193d8c4bb4492408a90e956d888b27eed14c698387c9b38230da3dad78058f"},
+ {file = "flake8_tidy_imports-4.6.0-py3-none-any.whl", hash = "sha256:6ae9f55d628156e19d19f4c359dd5d3e95431a9bd514f5e2748c53c1398c66b2"},
]
flake8-todo = [
{file = "flake8-todo-0.7.tar.gz", hash = "sha256:6e4c5491ff838c06fe5a771b0e95ee15fc005ca57196011011280fc834a85915"},
]
gitdb = [
- {file = "gitdb-4.0.7-py3-none-any.whl", hash = "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0"},
- {file = "gitdb-4.0.7.tar.gz", hash = "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"},
+ {file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"},
+ {file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"},
]
gitpython = [
- {file = "GitPython-3.1.24-py3-none-any.whl", hash = "sha256:dc0a7f2f697657acc8d7f89033e8b1ea94dd90356b2983bca89dc8d2ab3cc647"},
- {file = "GitPython-3.1.24.tar.gz", hash = "sha256:df83fdf5e684fef7c6ee2c02fc68a5ceb7e7e759d08b694088d0cacb4eba59e5"},
+ {file = "GitPython-3.1.26-py3-none-any.whl", hash = "sha256:26ac35c212d1f7b16036361ca5cff3ec66e11753a0d677fb6c48fa4e1a9dd8d6"},
+ {file = "GitPython-3.1.26.tar.gz", hash = "sha256:fc8868f63a2e6d268fb25f481995ba185a85a66fcad126f039323ff6635669ee"},
]
gunicorn = [
{file = "gunicorn-20.0.4-py2.py3-none-any.whl", hash = "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"},
{file = "gunicorn-20.0.4.tar.gz", hash = "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626"},
]
identify = [
- {file = "identify-2.3.0-py2.py3-none-any.whl", hash = "sha256:d1e82c83d063571bb88087676f81261a4eae913c492dafde184067c584bc7c05"},
- {file = "identify-2.3.0.tar.gz", hash = "sha256:fd08c97f23ceee72784081f1ce5125c8f53a02d3f2716dde79a6ab8f1039fea5"},
+ {file = "identify-2.4.6-py2.py3-none-any.whl", hash = "sha256:cf06b1639e0dca0c184b1504d8b73448c99a68e004a80524c7923b95f7b6837c"},
+ {file = "identify-2.4.6.tar.gz", hash = "sha256:233679e3f61a02015d4293dbccf16aa0e4996f868bd114688b8c124f18826706"},
]
idna = [
- {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"},
- {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"},
+ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
+ {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
+]
+importlib-metadata = [
+ {file = "importlib_metadata-4.10.1-py3-none-any.whl", hash = "sha256:899e2a40a8c4a1aec681feef45733de8a6c58f3f6a0dbed2eb6574b4387a77b6"},
+ {file = "importlib_metadata-4.10.1.tar.gz", hash = "sha256:951f0d8a5b7260e9db5e41d429285b5f451e928479f19d80818878527d36e95e"},
]
libsass = [
{file = "libsass-0.21.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:06c8776417fe930714bdc930a3d7e795ae3d72be6ac883ff72a1b8f7c49e5ffb"},
@@ -967,11 +980,12 @@ libsass = [
{file = "libsass-0.21.0-cp36-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e2b1a7d093f2e76dc694c17c0c285e846d0b0deb0e8b21dc852ba1a3a4e2f1d6"},
{file = "libsass-0.21.0-cp36-abi3-win32.whl", hash = "sha256:abc29357ee540849faf1383e1746d40d69ed5cb6d4c346df276b258f5aa8977a"},
{file = "libsass-0.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:659ae41af8708681fa3ec73f47b9735a6725e71c3b66ff570bfce78952f2314e"},
+ {file = "libsass-0.21.0-cp38-abi3-macosx_12_0_arm64.whl", hash = "sha256:c9ec490609752c1d81ff6290da33485aa7cb6d7365ac665b74464c1b7d97f7da"},
{file = "libsass-0.21.0.tar.gz", hash = "sha256:d5ba529d9ce668be9380563279f3ffe988f27bc5b299c5a28453df2e0b0fbaf2"},
]
markdown = [
- {file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"},
- {file = "Markdown-3.3.4.tar.gz", hash = "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49"},
+ {file = "Markdown-3.3.6-py3-none-any.whl", hash = "sha256:9923332318f843411e9932237530df53162e29dc7a4e2b91e35764583c46c9a3"},
+ {file = "Markdown-3.3.6.tar.gz", hash = "sha256:76df8ae32294ec39dcf89340382882dfa12975f87f45c3ed1ecdb1e8cefc7006"},
]
mccabe = [
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
@@ -986,54 +1000,58 @@ nodeenv = [
{file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"},
]
pbr = [
- {file = "pbr-5.6.0-py2.py3-none-any.whl", hash = "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"},
- {file = "pbr-5.6.0.tar.gz", hash = "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd"},
+ {file = "pbr-5.8.0-py2.py3-none-any.whl", hash = "sha256:176e8560eaf61e127817ef93d8a844803abb27a4d4637f0ff3bb783129be2e0a"},
+ {file = "pbr-5.8.0.tar.gz", hash = "sha256:672d8ebee84921862110f23fcec2acea191ef58543d34dfe9ef3d9f13c31cddf"},
]
pep8-naming = [
{file = "pep8-naming-0.12.1.tar.gz", hash = "sha256:bb2455947757d162aa4cad55dba4ce029005cd1692f2899a21d51d8630ca7841"},
{file = "pep8_naming-0.12.1-py2.py3-none-any.whl", hash = "sha256:4a8daeaeb33cfcde779309fc0c9c0a68a3bbe2ad8a8308b763c5068f86eb9f37"},
]
platformdirs = [
- {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"},
- {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"},
+ {file = "platformdirs-2.4.1-py3-none-any.whl", hash = "sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca"},
+ {file = "platformdirs-2.4.1.tar.gz", hash = "sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda"},
]
pre-commit = [
- {file = "pre_commit-2.15.0-py2.py3-none-any.whl", hash = "sha256:a4ed01000afcb484d9eb8d504272e642c4c4099bbad3a6b27e519bd6a3e928a6"},
- {file = "pre_commit-2.15.0.tar.gz", hash = "sha256:3c25add78dbdfb6a28a651780d5c311ac40dd17f160eb3954a0c59da40a505a7"},
+ {file = "pre_commit-2.17.0-py2.py3-none-any.whl", hash = "sha256:725fa7459782d7bec5ead072810e47351de01709be838c2ce1726b9591dad616"},
+ {file = "pre_commit-2.17.0.tar.gz", hash = "sha256:c1a8040ff15ad3d648c70cc3e55b93e4d2d5b687320955505587fd79bbaed06a"},
]
prometheus-client = [
- {file = "prometheus_client-0.11.0-py2.py3-none-any.whl", hash = "sha256:b014bc76815eb1399da8ce5fc84b7717a3e63652b0c0f8804092c9363acab1b2"},
- {file = "prometheus_client-0.11.0.tar.gz", hash = "sha256:3a8baade6cb80bcfe43297e33e7623f3118d660d41387593758e2fb1ea173a86"},
+ {file = "prometheus_client-0.13.1-py3-none-any.whl", hash = "sha256:357a447fd2359b0a1d2e9b311a0c5778c330cfbe186d880ad5a6b39884652316"},
+ {file = "prometheus_client-0.13.1.tar.gz", hash = "sha256:ada41b891b79fca5638bd5cfe149efa86512eaa55987893becd2c6d8d0a5dfc5"},
]
psutil = [
- {file = "psutil-5.8.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0066a82f7b1b37d334e68697faba68e5ad5e858279fd6351c8ca6024e8d6ba64"},
- {file = "psutil-5.8.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:0ae6f386d8d297177fd288be6e8d1afc05966878704dad9847719650e44fc49c"},
- {file = "psutil-5.8.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:12d844996d6c2b1d3881cfa6fa201fd635971869a9da945cf6756105af73d2df"},
- {file = "psutil-5.8.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:02b8292609b1f7fcb34173b25e48d0da8667bc85f81d7476584d889c6e0f2131"},
- {file = "psutil-5.8.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6ffe81843131ee0ffa02c317186ed1e759a145267d54fdef1bc4ea5f5931ab60"},
- {file = "psutil-5.8.0-cp27-none-win32.whl", hash = "sha256:ea313bb02e5e25224e518e4352af4bf5e062755160f77e4b1767dd5ccb65f876"},
- {file = "psutil-5.8.0-cp27-none-win_amd64.whl", hash = "sha256:5da29e394bdedd9144c7331192e20c1f79283fb03b06e6abd3a8ae45ffecee65"},
- {file = "psutil-5.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:74fb2557d1430fff18ff0d72613c5ca30c45cdbfcddd6a5773e9fc1fe9364be8"},
- {file = "psutil-5.8.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:74f2d0be88db96ada78756cb3a3e1b107ce8ab79f65aa885f76d7664e56928f6"},
- {file = "psutil-5.8.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:99de3e8739258b3c3e8669cb9757c9a861b2a25ad0955f8e53ac662d66de61ac"},
- {file = "psutil-5.8.0-cp36-cp36m-win32.whl", hash = "sha256:36b3b6c9e2a34b7d7fbae330a85bf72c30b1c827a4366a07443fc4b6270449e2"},
- {file = "psutil-5.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:52de075468cd394ac98c66f9ca33b2f54ae1d9bff1ef6b67a212ee8f639ec06d"},
- {file = "psutil-5.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c6a5fd10ce6b6344e616cf01cc5b849fa8103fbb5ba507b6b2dee4c11e84c935"},
- {file = "psutil-5.8.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:61f05864b42fedc0771d6d8e49c35f07efd209ade09a5afe6a5059e7bb7bf83d"},
- {file = "psutil-5.8.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:0dd4465a039d343925cdc29023bb6960ccf4e74a65ad53e768403746a9207023"},
- {file = "psutil-5.8.0-cp37-cp37m-win32.whl", hash = "sha256:1bff0d07e76114ec24ee32e7f7f8d0c4b0514b3fae93e3d2aaafd65d22502394"},
- {file = "psutil-5.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:fcc01e900c1d7bee2a37e5d6e4f9194760a93597c97fee89c4ae51701de03563"},
- {file = "psutil-5.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6223d07a1ae93f86451d0198a0c361032c4c93ebd4bf6d25e2fb3edfad9571ef"},
- {file = "psutil-5.8.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d225cd8319aa1d3c85bf195c4e07d17d3cd68636b8fc97e6cf198f782f99af28"},
- {file = "psutil-5.8.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:28ff7c95293ae74bf1ca1a79e8805fcde005c18a122ca983abf676ea3466362b"},
- {file = "psutil-5.8.0-cp38-cp38-win32.whl", hash = "sha256:ce8b867423291cb65cfc6d9c4955ee9bfc1e21fe03bb50e177f2b957f1c2469d"},
- {file = "psutil-5.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:90f31c34d25b1b3ed6c40cdd34ff122b1887a825297c017e4cbd6796dd8b672d"},
- {file = "psutil-5.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6323d5d845c2785efb20aded4726636546b26d3b577aded22492908f7c1bdda7"},
- {file = "psutil-5.8.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:245b5509968ac0bd179287d91210cd3f37add77dad385ef238b275bad35fa1c4"},
- {file = "psutil-5.8.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:90d4091c2d30ddd0a03e0b97e6a33a48628469b99585e2ad6bf21f17423b112b"},
- {file = "psutil-5.8.0-cp39-cp39-win32.whl", hash = "sha256:ea372bcc129394485824ae3e3ddabe67dc0b118d262c568b4d2602a7070afdb0"},
- {file = "psutil-5.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:f4634b033faf0d968bb9220dd1c793b897ab7f1189956e1aa9eae752527127d3"},
- {file = "psutil-5.8.0.tar.gz", hash = "sha256:0c9ccb99ab76025f2f0bbecf341d4656e9c1351db8cc8a03ccd62e318ab4b5c6"},
+ {file = "psutil-5.9.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:55ce319452e3d139e25d6c3f85a1acf12d1607ddedea5e35fb47a552c051161b"},
+ {file = "psutil-5.9.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:7336292a13a80eb93c21f36bde4328aa748a04b68c13d01dfddd67fc13fd0618"},
+ {file = "psutil-5.9.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:cb8d10461c1ceee0c25a64f2dd54872b70b89c26419e147a05a10b753ad36ec2"},
+ {file = "psutil-5.9.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:7641300de73e4909e5d148e90cc3142fb890079e1525a840cf0dfd39195239fd"},
+ {file = "psutil-5.9.0-cp27-none-win32.whl", hash = "sha256:ea42d747c5f71b5ccaa6897b216a7dadb9f52c72a0fe2b872ef7d3e1eacf3ba3"},
+ {file = "psutil-5.9.0-cp27-none-win_amd64.whl", hash = "sha256:ef216cc9feb60634bda2f341a9559ac594e2eeaadd0ba187a4c2eb5b5d40b91c"},
+ {file = "psutil-5.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90a58b9fcae2dbfe4ba852b57bd4a1dded6b990a33d6428c7614b7d48eccb492"},
+ {file = "psutil-5.9.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff0d41f8b3e9ebb6b6110057e40019a432e96aae2008951121ba4e56040b84f3"},
+ {file = "psutil-5.9.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:742c34fff804f34f62659279ed5c5b723bb0195e9d7bd9907591de9f8f6558e2"},
+ {file = "psutil-5.9.0-cp310-cp310-win32.whl", hash = "sha256:8293942e4ce0c5689821f65ce6522ce4786d02af57f13c0195b40e1edb1db61d"},
+ {file = "psutil-5.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:9b51917c1af3fa35a3f2dabd7ba96a2a4f19df3dec911da73875e1edaf22a40b"},
+ {file = "psutil-5.9.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e9805fed4f2a81de98ae5fe38b75a74c6e6ad2df8a5c479594c7629a1fe35f56"},
+ {file = "psutil-5.9.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c51f1af02334e4b516ec221ee26b8fdf105032418ca5a5ab9737e8c87dafe203"},
+ {file = "psutil-5.9.0-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32acf55cb9a8cbfb29167cd005951df81b567099295291bcfd1027365b36591d"},
+ {file = "psutil-5.9.0-cp36-cp36m-win32.whl", hash = "sha256:e5c783d0b1ad6ca8a5d3e7b680468c9c926b804be83a3a8e95141b05c39c9f64"},
+ {file = "psutil-5.9.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d62a2796e08dd024b8179bd441cb714e0f81226c352c802fca0fd3f89eeacd94"},
+ {file = "psutil-5.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3d00a664e31921009a84367266b35ba0aac04a2a6cad09c550a89041034d19a0"},
+ {file = "psutil-5.9.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7779be4025c540d1d65a2de3f30caeacc49ae7a2152108adeaf42c7534a115ce"},
+ {file = "psutil-5.9.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:072664401ae6e7c1bfb878c65d7282d4b4391f1bc9a56d5e03b5a490403271b5"},
+ {file = "psutil-5.9.0-cp37-cp37m-win32.whl", hash = "sha256:df2c8bd48fb83a8408c8390b143c6a6fa10cb1a674ca664954de193fdcab36a9"},
+ {file = "psutil-5.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1d7b433519b9a38192dfda962dd8f44446668c009833e1429a52424624f408b4"},
+ {file = "psutil-5.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c3400cae15bdb449d518545cbd5b649117de54e3596ded84aacabfbb3297ead2"},
+ {file = "psutil-5.9.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2237f35c4bbae932ee98902a08050a27821f8f6dfa880a47195e5993af4702d"},
+ {file = "psutil-5.9.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1070a9b287846a21a5d572d6dddd369517510b68710fca56b0e9e02fd24bed9a"},
+ {file = "psutil-5.9.0-cp38-cp38-win32.whl", hash = "sha256:76cebf84aac1d6da5b63df11fe0d377b46b7b500d892284068bacccf12f20666"},
+ {file = "psutil-5.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:3151a58f0fbd8942ba94f7c31c7e6b310d2989f4da74fcbf28b934374e9bf841"},
+ {file = "psutil-5.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:539e429da49c5d27d5a58e3563886057f8fc3868a5547b4f1876d9c0f007bccf"},
+ {file = "psutil-5.9.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58c7d923dc209225600aec73aa2c4ae8ea33b1ab31bc11ef8a5933b027476f07"},
+ {file = "psutil-5.9.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3611e87eea393f779a35b192b46a164b1d01167c9d323dda9b1e527ea69d697d"},
+ {file = "psutil-5.9.0-cp39-cp39-win32.whl", hash = "sha256:4e2fb92e3aeae3ec3b7b66c528981fd327fb93fd906a77215200404444ec1845"},
+ {file = "psutil-5.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:7d190ee2eaef7831163f254dc58f6d2e2a22e27382b936aab51c835fc080c3d3"},
+ {file = "psutil-5.9.0.tar.gz", hash = "sha256:869842dbd66bb80c3217158e629d6fceaecc3a3166d3d1faee515b05dd26ca25"},
]
psycopg2-binary = [
{file = "psycopg2-binary-2.8.6.tar.gz", hash = "sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0"},
@@ -1081,8 +1099,8 @@ pydocstyle = [
{file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"},
]
pyfakefs = [
- {file = "pyfakefs-4.4.0-py3-none-any.whl", hash = "sha256:1ac3b2845dabe69af56c20691b9347914581195ccdde352535fb7d4ff0055c19"},
- {file = "pyfakefs-4.4.0.tar.gz", hash = "sha256:082d863e0e2a74351f697da404e329a91e18e5055942e59d1b836e8459b2c94c"},
+ {file = "pyfakefs-4.5.4-py3-none-any.whl", hash = "sha256:e0cc0d22cb74badf4fb2143a112817d7aea1a58ee9dca015a68bf38c3691cb52"},
+ {file = "pyfakefs-4.5.4.tar.gz", hash = "sha256:5b5951e873f73bf12e3a19d8e4470c4b7962c51df753cf8c4caaf64e24a0a323"},
]
pyflakes = [
{file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"},
@@ -1132,8 +1150,8 @@ pyyaml = [
{file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"},
]
requests = [
- {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
- {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"},
+ {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"},
+ {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"},
]
sentry-sdk = [
{file = "sentry-sdk-0.20.3.tar.gz", hash = "sha256:4ae8d1ced6c67f1c8ea51d82a16721c166c489b76876c9f2c202b8a50334b237"},
@@ -1144,20 +1162,20 @@ six = [
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
smmap = [
- {file = "smmap-4.0.0-py2.py3-none-any.whl", hash = "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"},
- {file = "smmap-4.0.0.tar.gz", hash = "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182"},
+ {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"},
+ {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"},
]
snowballstemmer = [
- {file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"},
- {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"},
+ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"},
+ {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"},
]
sqlparse = [
{file = "sqlparse-0.4.2-py3-none-any.whl", hash = "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"},
{file = "sqlparse-0.4.2.tar.gz", hash = "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae"},
]
stevedore = [
- {file = "stevedore-3.4.0-py3-none-any.whl", hash = "sha256:920ce6259f0b2498aaa4545989536a27e4e4607b8318802d7ddc3a533d3d069e"},
- {file = "stevedore-3.4.0.tar.gz", hash = "sha256:59b58edb7f57b11897f150475e7bc0c39c5381f0b8e3fa9f5c20ce6c89ec4aa1"},
+ {file = "stevedore-3.5.0-py3-none-any.whl", hash = "sha256:a547de73308fd7e90075bb4d301405bebf705292fa90a90fc3bcf9133f58616c"},
+ {file = "stevedore-3.5.0.tar.gz", hash = "sha256:f40253887d8712eaa2bb0ea3830374416736dc8ec0e22f5a65092c1174c44335"},
]
taskipy = [
{file = "taskipy-1.7.0-py3-none-any.whl", hash = "sha256:9e284c10898e9dee01a3e72220b94b192b1daa0f560271503a6df1da53d03844"},
@@ -1167,20 +1185,19 @@ toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
-typing-extensions = [
- {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
- {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
- {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
-]
urllib3 = [
- {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"},
- {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"},
+ {file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"},
+ {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"},
]
virtualenv = [
- {file = "virtualenv-20.8.1-py2.py3-none-any.whl", hash = "sha256:10062e34c204b5e4ec5f62e6ef2473f8ba76513a9a617e873f1f8fb4a519d300"},
- {file = "virtualenv-20.8.1.tar.gz", hash = "sha256:bcc17f0b3a29670dd777d6f0755a4c04f28815395bca279cdcb213b97199a6b8"},
+ {file = "virtualenv-20.13.0-py2.py3-none-any.whl", hash = "sha256:339f16c4a86b44240ba7223d0f93a7887c3ca04b5f9c8129da7958447d079b09"},
+ {file = "virtualenv-20.13.0.tar.gz", hash = "sha256:d8458cf8d59d0ea495ad9b34c2599487f8a7772d796f9910858376d1600dd2dd"},
]
whitenoise = [
{file = "whitenoise-5.3.0-py2.py3-none-any.whl", hash = "sha256:d963ef25639d1417e8a247be36e6aedd8c7c6f0a08adcb5a89146980a96b577c"},
{file = "whitenoise-5.3.0.tar.gz", hash = "sha256:d234b871b52271ae7ed6d9da47ffe857c76568f11dd30e28e18c5869dbd11e12"},
]
+zipp = [
+ {file = "zipp-3.7.0-py3-none-any.whl", hash = "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"},
+ {file = "zipp-3.7.0.tar.gz", hash = "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d"},
+]
diff --git a/pydis_site/apps/api/__init__.py b/pydis_site/apps/api/__init__.py
index e69de29b..afa5b4d5 100644
--- a/pydis_site/apps/api/__init__.py
+++ b/pydis_site/apps/api/__init__.py
@@ -0,0 +1 @@
+default_app_config = 'pydis_site.apps.api.apps.ApiConfig'
diff --git a/pydis_site/apps/api/apps.py b/pydis_site/apps/api/apps.py
index 76810b2e..18eda9e3 100644
--- a/pydis_site/apps/api/apps.py
+++ b/pydis_site/apps/api/apps.py
@@ -4,4 +4,12 @@ from django.apps import AppConfig
class ApiConfig(AppConfig):
"""Django AppConfig for the API app."""
- name = 'api'
+ name = 'pydis_site.apps.api'
+
+ def ready(self) -> None:
+ """
+ Gets called as soon as the registry is fully populated.
+
+ https://docs.djangoproject.com/en/3.2/ref/applications/#django.apps.AppConfig.ready
+ """
+ import pydis_site.apps.api.signals # noqa: F401
diff --git a/pydis_site/apps/api/migrations/0059_populate_filterlists.py b/pydis_site/apps/api/migrations/0059_populate_filterlists.py
index 8c550191..273db3d1 100644
--- a/pydis_site/apps/api/migrations/0059_populate_filterlists.py
+++ b/pydis_site/apps/api/migrations/0059_populate_filterlists.py
@@ -60,35 +60,35 @@ domain_name_blacklist = [
]
filter_token_blacklist = [
- ("\bgoo+ks*\b", None, False),
- ("\bky+s+\b", None, False),
- ("\bki+ke+s*\b", None, False),
- ("\bbeaner+s?\b", None, False),
- ("\bcoo+ns*\b", None, False),
- ("\bnig+lets*\b", None, False),
- ("\bslant-eyes*\b", None, False),
- ("\btowe?l-?head+s*\b", None, False),
- ("\bchi*n+k+s*\b", None, False),
- ("\bspick*s*\b", None, False),
- ("\bkill* +(?:yo)?urself+\b", None, False),
- ("\bjew+s*\b", None, False),
- ("\bsuicide\b", None, False),
- ("\brape\b", None, False),
- ("\b(re+)tar+(d+|t+)(ed)?\b", None, False),
- ("\bta+r+d+\b", None, False),
- ("\bcunts*\b", None, False),
- ("\btrann*y\b", None, False),
- ("\bshemale\b", None, False),
- ("fa+g+s*", None, False),
- ("卐", None, False),
- ("卍", None, False),
- ("࿖", None, False),
- ("࿕", None, False),
- ("࿘", None, False),
- ("࿗", None, False),
- ("cuck(?!oo+)", None, False),
- ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False),
- ("fag+o+t+s*", None, False),
+ (r"\bgoo+ks*\b", None, False),
+ (r"\bky+s+\b", None, False),
+ (r"\bki+ke+s*\b", None, False),
+ (r"\bbeaner+s?\b", None, False),
+ (r"\bcoo+ns*\b", None, False),
+ (r"\bnig+lets*\b", None, False),
+ (r"\bslant-eyes*\b", None, False),
+ (r"\btowe?l-?head+s*\b", None, False),
+ (r"\bchi*n+k+s*\b", None, False),
+ (r"\bspick*s*\b", None, False),
+ (r"\bkill* +(?:yo)?urself+\b", None, False),
+ (r"\bjew+s*\b", None, False),
+ (r"\bsuicide\b", None, False),
+ (r"\brape\b", None, False),
+ (r"\b(re+)tar+(d+|t+)(ed)?\b", None, False),
+ (r"\bta+r+d+\b", None, False),
+ (r"\bcunts*\b", None, False),
+ (r"\btrann*y\b", None, False),
+ (r"\bshemale\b", None, False),
+ (r"fa+g+s*", None, False),
+ (r"卐", None, False),
+ (r"卍", None, False),
+ (r"࿖", None, False),
+ (r"࿕", None, False),
+ (r"࿘", None, False),
+ (r"࿗", None, False),
+ (r"cuck(?!oo+)", None, False),
+ (r"nigg+(?:e*r+|a+h*?|u+h+)s?", None, False),
+ (r"fag+o+t+s*", None, False),
]
file_format_whitelist = [
diff --git a/pydis_site/apps/api/migrations/0070_auto_20210519_0545.py b/pydis_site/apps/api/migrations/0070_auto_20210519_0545.py
new file mode 100644
index 00000000..dbd7ac91
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0070_auto_20210519_0545.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.0.14 on 2021-05-19 05:45
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0069_documentationlink_validators'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='offtopicchannelname',
+ name='active',
+ field=models.BooleanField(default=True, help_text='Whether or not this name should be considered for naming channels.'),
+ ),
+ migrations.AlterField(
+ model_name='offtopicchannelname',
+ name='used',
+ field=models.BooleanField(default=False, help_text='Whether or not this name has already been used during this rotation.'),
+ ),
+ ]
diff --git a/pydis_site/apps/api/migrations/0072_merge_20210724_1354.py b/pydis_site/apps/api/migrations/0072_merge_20210724_1354.py
new file mode 100644
index 00000000..f12efab5
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0072_merge_20210724_1354.py
@@ -0,0 +1,14 @@
+# Generated by Django 3.0.14 on 2021-07-24 13:54
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0071_increase_message_content_4000'),
+ ('api', '0070_auto_20210519_0545'),
+ ]
+
+ operations = [
+ ]
diff --git a/pydis_site/apps/api/migrations/0074_merge_20211105_0518.py b/pydis_site/apps/api/migrations/0074_merge_20211105_0518.py
new file mode 100644
index 00000000..ebf5ae15
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0074_merge_20211105_0518.py
@@ -0,0 +1,14 @@
+# Generated by Django 3.0.14 on 2021-11-05 05:18
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0072_merge_20210724_1354'),
+ ('api', '0073_otn_allow_GT_and_LT'),
+ ]
+
+ operations = [
+ ]
diff --git a/pydis_site/apps/api/migrations/0074_voice_mute.py b/pydis_site/apps/api/migrations/0074_voice_mute.py
new file mode 100644
index 00000000..937557bc
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0074_voice_mute.py
@@ -0,0 +1,36 @@
+# Generated by Django 3.0.14 on 2021-10-09 18:52
+from django.apps.registry import Apps
+from django.db import migrations, models
+from django.db.backends.base.schema import BaseDatabaseSchemaEditor
+
+
+def migrate_infractions(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None:
+ Infraction = apps.get_model("api", "Infraction")
+
+ for infraction in Infraction.objects.filter(type="voice_ban"):
+ infraction.type = "voice_mute"
+ infraction.save()
+
+
+def unmigrate_infractions(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None:
+ Infraction = apps.get_model("api", "Infraction")
+
+ for infraction in Infraction.objects.filter(type="voice_mute"):
+ infraction.type = "voice_ban"
+ infraction.save()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0073_otn_allow_GT_and_LT'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='infraction',
+ name='type',
+ field=models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('watch', 'Watch'), ('mute', 'Mute'), ('kick', 'Kick'), ('ban', 'Ban'), ('superstar', 'Superstar'), ('voice_ban', 'Voice Ban'), ('voice_mute', 'Voice Mute')], help_text='The type of the infraction.', max_length=10),
+ ),
+ migrations.RunPython(migrate_infractions, unmigrate_infractions)
+ ]
diff --git a/pydis_site/apps/api/migrations/0075_add_redirects_filter.py b/pydis_site/apps/api/migrations/0075_add_redirects_filter.py
new file mode 100644
index 00000000..23dc176f
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0075_add_redirects_filter.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.0.14 on 2021-11-17 10:24
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0074_reminder_failures'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='filterlist',
+ name='type',
+ field=models.CharField(choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token'), ('REDIRECT', 'Redirect')], help_text='The type of allowlist this is on.', max_length=50),
+ ),
+ ]
diff --git a/pydis_site/apps/api/migrations/0075_infraction_dm_sent.py b/pydis_site/apps/api/migrations/0075_infraction_dm_sent.py
new file mode 100644
index 00000000..c0ac709d
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0075_infraction_dm_sent.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.0.14 on 2021-11-10 22:06
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0074_reminder_failures'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='infraction',
+ name='dm_sent',
+ field=models.BooleanField(help_text='Whether a DM was sent to the user when infraction was applied.', null=True),
+ ),
+ ]
diff --git a/pydis_site/apps/api/migrations/0076_merge_20211125_1941.py b/pydis_site/apps/api/migrations/0076_merge_20211125_1941.py
new file mode 100644
index 00000000..097d0a0c
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0076_merge_20211125_1941.py
@@ -0,0 +1,14 @@
+# Generated by Django 3.0.14 on 2021-11-25 19:41
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0075_infraction_dm_sent'),
+ ('api', '0075_add_redirects_filter'),
+ ]
+
+ operations = [
+ ]
diff --git a/pydis_site/apps/api/migrations/0077_use_generic_jsonfield.py b/pydis_site/apps/api/migrations/0077_use_generic_jsonfield.py
new file mode 100644
index 00000000..9e8f2fb9
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0077_use_generic_jsonfield.py
@@ -0,0 +1,25 @@
+# Generated by Django 3.1.13 on 2021-11-27 12:27
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+import pydis_site.apps.api.models.utils
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0076_merge_20211125_1941'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='botsetting',
+ name='data',
+ field=models.JSONField(help_text='The actual settings of this setting.'),
+ ),
+ migrations.AlterField(
+ model_name='deletedmessage',
+ name='embeds',
+ field=django.contrib.postgres.fields.ArrayField(base_field=models.JSONField(validators=[pydis_site.apps.api.models.utils.validate_embed]), blank=True, help_text='Embeds attached to this message.', size=None),
+ ),
+ ]
diff --git a/pydis_site/apps/api/migrations/0078_merge_20211213_0552.py b/pydis_site/apps/api/migrations/0078_merge_20211213_0552.py
new file mode 100644
index 00000000..5ce0e871
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0078_merge_20211213_0552.py
@@ -0,0 +1,14 @@
+# Generated by Django 3.1.14 on 2021-12-13 05:52
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0077_use_generic_jsonfield'),
+ ('api', '0074_merge_20211105_0518'),
+ ]
+
+ operations = [
+ ]
diff --git a/pydis_site/apps/api/migrations/0079_merge_20220125_2022.py b/pydis_site/apps/api/migrations/0079_merge_20220125_2022.py
new file mode 100644
index 00000000..9b9d9326
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0079_merge_20220125_2022.py
@@ -0,0 +1,14 @@
+# Generated by Django 3.1.14 on 2022-01-25 20:22
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0078_merge_20211213_0552'),
+ ('api', '0074_voice_mute'),
+ ]
+
+ operations = [
+ ]
diff --git a/pydis_site/apps/api/models/bot/bot_setting.py b/pydis_site/apps/api/models/bot/bot_setting.py
index 2a3944f8..1bcb1ae6 100644
--- a/pydis_site/apps/api/models/bot/bot_setting.py
+++ b/pydis_site/apps/api/models/bot/bot_setting.py
@@ -1,4 +1,3 @@
-from django.contrib.postgres import fields as pgfields
from django.core.exceptions import ValidationError
from django.db import models
@@ -24,6 +23,6 @@ class BotSetting(ModelReprMixin, models.Model):
max_length=50,
validators=(validate_bot_setting_name,)
)
- data = pgfields.JSONField(
+ data = models.JSONField(
help_text="The actual settings of this setting."
)
diff --git a/pydis_site/apps/api/models/bot/filter_list.py b/pydis_site/apps/api/models/bot/filter_list.py
index d279e137..d30f7213 100644
--- a/pydis_site/apps/api/models/bot/filter_list.py
+++ b/pydis_site/apps/api/models/bot/filter_list.py
@@ -12,6 +12,7 @@ class FilterList(ModelTimestampMixin, ModelReprMixin, models.Model):
'FILE_FORMAT '
'DOMAIN_NAME '
'FILTER_TOKEN '
+ 'REDIRECT '
)
type = models.CharField(
max_length=50,
diff --git a/pydis_site/apps/api/models/bot/infraction.py b/pydis_site/apps/api/models/bot/infraction.py
index 60c1e8dd..c9303024 100644
--- a/pydis_site/apps/api/models/bot/infraction.py
+++ b/pydis_site/apps/api/models/bot/infraction.py
@@ -17,6 +17,7 @@ class Infraction(ModelReprMixin, models.Model):
("ban", "Ban"),
("superstar", "Superstar"),
("voice_ban", "Voice Ban"),
+ ("voice_mute", "Voice Mute"),
)
inserted_at = models.DateTimeField(
default=timezone.now,
@@ -45,7 +46,7 @@ class Infraction(ModelReprMixin, models.Model):
help_text="The user which applied the infraction."
)
type = models.CharField(
- max_length=9,
+ max_length=10,
choices=TYPE_CHOICES,
help_text="The type of the infraction."
)
@@ -57,6 +58,10 @@ class Infraction(ModelReprMixin, models.Model):
default=False,
help_text="Whether the infraction is a shadow infraction."
)
+ dm_sent = models.BooleanField(
+ null=True,
+ help_text="Whether a DM was sent to the user when infraction was applied."
+ )
def __str__(self):
"""Returns some info on the current infraction, for display purposes."""
diff --git a/pydis_site/apps/api/models/bot/message.py b/pydis_site/apps/api/models/bot/message.py
index 60e2a553..bab3368d 100644
--- a/pydis_site/apps/api/models/bot/message.py
+++ b/pydis_site/apps/api/models/bot/message.py
@@ -48,7 +48,7 @@ class Message(ModelReprMixin, models.Model):
blank=True
)
embeds = pgfields.ArrayField(
- pgfields.JSONField(
+ models.JSONField(
validators=(validate_embed,)
),
blank=True,
diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py
index 33fb7ad7..abd25ef0 100644
--- a/pydis_site/apps/api/models/bot/metricity.py
+++ b/pydis_site/apps/api/models/bot/metricity.py
@@ -4,13 +4,13 @@ from django.db import connections
BLOCK_INTERVAL = 10 * 60 # 10 minute blocks
-EXCLUDE_CHANNELS = [
+EXCLUDE_CHANNELS = (
"267659945086812160", # Bot commands
"607247579608121354" # SeasonalBot commands
-]
+)
-class NotFoundError(Exception):
+class NotFoundError(Exception): # noqa: N818
"""Raised when an entity cannot be found."""
pass
@@ -46,12 +46,12 @@ class Metricity:
self.cursor.execute(
"""
SELECT
- COUNT(*)
+ COUNT(*)
FROM messages
WHERE
- author_id = '%s'
- AND NOT is_deleted
- AND NOT %s::varchar[] @> ARRAY[channel_id]
+ author_id = '%s'
+ AND NOT is_deleted
+ AND channel_id NOT IN %s
""",
[user_id, EXCLUDE_CHANNELS]
)
@@ -79,7 +79,7 @@ class Metricity:
WHERE
author_id='%s'
AND NOT is_deleted
- AND NOT %s::varchar[] @> ARRAY[channel_id]
+ AND channel_id NOT IN %s
GROUP BY interval
) block_query;
""",
diff --git a/pydis_site/apps/api/models/bot/off_topic_channel_name.py b/pydis_site/apps/api/models/bot/off_topic_channel_name.py
index 8999e560..e9fec114 100644
--- a/pydis_site/apps/api/models/bot/off_topic_channel_name.py
+++ b/pydis_site/apps/api/models/bot/off_topic_channel_name.py
@@ -18,7 +18,12 @@ class OffTopicChannelName(ModelReprMixin, models.Model):
used = models.BooleanField(
default=False,
- help_text="Whether or not this name has already been used during this rotation",
+ help_text="Whether or not this name has already been used during this rotation.",
+ )
+
+ active = models.BooleanField(
+ default=True,
+ help_text="Whether or not this name should be considered for naming channels."
)
def __str__(self):
diff --git a/pydis_site/apps/api/models/utils.py b/pydis_site/apps/api/models/utils.py
index 0e220a1d..859394d2 100644
--- a/pydis_site/apps/api/models/utils.py
+++ b/pydis_site/apps/api/models/utils.py
@@ -103,11 +103,10 @@ def validate_embed(embed: Any) -> None:
Example:
- >>> from django.contrib.postgres import fields as pgfields
>>> from django.db import models
>>> from pydis_site.apps.api.models.utils import validate_embed
>>> class MyMessage(models.Model):
- ... embed = pgfields.JSONField(
+ ... embed = models.JSONField(
... validators=(
... validate_embed,
... )
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index 3e213d43..4a702d61 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -145,7 +145,16 @@ class InfractionSerializer(ModelSerializer):
model = Infraction
fields = (
- 'id', 'inserted_at', 'expires_at', 'active', 'user', 'actor', 'type', 'reason', 'hidden'
+ 'id',
+ 'inserted_at',
+ 'expires_at',
+ 'active',
+ 'user',
+ 'actor',
+ 'type',
+ 'reason',
+ 'hidden',
+ 'dm_sent'
)
validators = [
UniqueTogetherValidator(
@@ -168,7 +177,7 @@ class InfractionSerializer(ModelSerializer):
raise ValidationError({'expires_at': [f'{infr_type} infractions cannot expire.']})
hidden = attrs.get('hidden')
- if hidden and infr_type in ('superstar', 'warning', 'voice_ban'):
+ if hidden and infr_type in ('superstar', 'warning', 'voice_ban', 'voice_mute'):
raise ValidationError({'hidden': [f'{infr_type} infractions cannot be hidden.']})
if not hidden and infr_type in ('note', ):
@@ -200,25 +209,30 @@ class ExpandedInfractionSerializer(InfractionSerializer):
return ret
+class OffTopicChannelNameListSerializer(ListSerializer):
+ """Custom ListSerializer to override to_representation() when list views are triggered."""
+
+ def to_representation(self, objects: list[OffTopicChannelName]) -> list[str]:
+ """
+ Return a list with all `OffTopicChannelName`s in the database.
+
+ This returns the list of off topic channel names. We want to only return
+ the name attribute, hence it is unnecessary to create a nested dictionary.
+ Additionally, this allows off topic channel name routes to simply return an
+ array of names instead of objects, saving on bandwidth.
+ """
+ return [obj.name for obj in objects]
+
+
class OffTopicChannelNameSerializer(ModelSerializer):
"""A class providing (de-)serialization of `OffTopicChannelName` instances."""
class Meta:
"""Metadata defined for the Django REST Framework."""
+ list_serializer_class = OffTopicChannelNameListSerializer
model = OffTopicChannelName
- fields = ('name',)
-
- def to_representation(self, obj: OffTopicChannelName) -> str:
- """
- Return the representation of this `OffTopicChannelName`.
-
- This only returns the name of the off topic channel name. As the model
- only has a single attribute, it is unnecessary to create a nested dictionary.
- Additionally, this allows off topic channel name routes to simply return an
- array of names instead of objects, saving on bandwidth.
- """
- return obj.name
+ fields = ('name', 'used', 'active')
class ReminderSerializer(ModelSerializer):
diff --git a/pydis_site/apps/api/signals.py b/pydis_site/apps/api/signals.py
new file mode 100644
index 00000000..5c26bfb6
--- /dev/null
+++ b/pydis_site/apps/api/signals.py
@@ -0,0 +1,12 @@
+from django.db.models.signals import post_delete
+from django.dispatch import receiver
+
+from pydis_site.apps.api.models.bot import Role, User
+
+
+@receiver(signal=post_delete, sender=Role)
+def delete_role_from_user(sender: Role, instance: Role, **kwargs) -> None:
+ """Unassigns the Role (instance) that is being deleted from every user that has it."""
+ for user in User.objects.filter(roles__contains=[instance.id]):
+ del user.roles[user.roles.index(instance.id)]
+ user.save()
diff --git a/pydis_site/apps/api/tests/test_off_topic_channel_names.py b/pydis_site/apps/api/tests/test_off_topic_channel_names.py
index 1825f6e6..34098c92 100644
--- a/pydis_site/apps/api/tests/test_off_topic_channel_names.py
+++ b/pydis_site/apps/api/tests/test_off_topic_channel_names.py
@@ -65,8 +65,18 @@ class EmptyDatabaseTests(AuthenticatedAPITestCase):
class ListTests(AuthenticatedAPITestCase):
@classmethod
def setUpTestData(cls):
- cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand', used=False)
- cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk', used=True)
+ cls.test_name = OffTopicChannelName.objects.create(
+ name='lemons-lemonade-stand', used=False, active=True
+ )
+ cls.test_name_2 = OffTopicChannelName.objects.create(
+ name='bbq-with-bisk', used=False, active=True
+ )
+ cls.test_name_3 = OffTopicChannelName.objects.create(
+ name="frozen-with-iceman", used=True, active=False
+ )
+ cls.test_name_4 = OffTopicChannelName.objects.create(
+ name="xith-is-cool", used=True, active=True
+ )
def test_returns_name_in_list(self):
"""Return all off-topic channel names."""
@@ -75,29 +85,73 @@ class ListTests(AuthenticatedAPITestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(
- response.json(),
- [
+ set(response.json()),
+ {
self.test_name.name,
- self.test_name_2.name
- ]
+ self.test_name_2.name,
+ self.test_name_3.name,
+ self.test_name_4.name
+ }
)
- def test_returns_single_item_with_random_items_param_set_to_1(self):
- """Return not-used name instead used."""
+ def test_returns_two_active_items_with_random_items_param_set_to_2(self):
+ """Return not-used active names instead used."""
url = reverse('api:bot:offtopicchannelname-list')
- response = self.client.get(f'{url}?random_items=1')
+ response = self.client.get(f'{url}?random_items=2')
self.assertEqual(response.status_code, 200)
- self.assertEqual(len(response.json()), 1)
- self.assertEqual(response.json(), [self.test_name.name])
+ self.assertEqual(len(response.json()), 2)
+ self.assertTrue(
+ all(
+ item in (self.test_name.name, self.test_name_2.name, self.test_name_4.name)
+ for item in response.json()
+ )
+ )
+
+ def test_returns_three_active_items_with_random_items_param_set_to_3(self):
+ """Return not-used active names instead used."""
+ url = reverse('api:bot:offtopicchannelname-list')
+ response = self.client.get(f'{url}?random_items=3')
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(len(response.json()), 3)
+ self.assertEqual(
+ set(response.json()),
+ {self.test_name.name, self.test_name_2.name, self.test_name_4.name}
+ )
def test_running_out_of_names_with_random_parameter(self):
- """Reset names `used` parameter to `False` when running out of names."""
+ """Reset names `used` parameter to `False` when running out of active names."""
url = reverse('api:bot:offtopicchannelname-list')
- response = self.client.get(f'{url}?random_items=2')
+ response = self.client.get(f'{url}?random_items=3')
self.assertEqual(response.status_code, 200)
- self.assertEqual(response.json(), [self.test_name.name, self.test_name_2.name])
+ self.assertEqual(
+ set(response.json()),
+ {self.test_name.name, self.test_name_2.name, self.test_name_4.name}
+ )
+
+ def test_returns_inactive_ot_names(self):
+ """Return inactive off topic names."""
+ url = reverse('api:bot:offtopicchannelname-list')
+ response = self.client.get(f"{url}?active=false")
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(
+ response.json(),
+ [self.test_name_3.name]
+ )
+
+ def test_returns_active_ot_names(self):
+ """Return active off topic names."""
+ url = reverse('api:bot:offtopicchannelname-list')
+ response = self.client.get(f"{url}?active=true")
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(
+ set(response.json()),
+ {self.test_name.name, self.test_name_2.name, self.test_name_4.name}
+ )
class CreationTests(AuthenticatedAPITestCase):
diff --git a/pydis_site/apps/api/tests/test_roles.py b/pydis_site/apps/api/tests/test_roles.py
index d39cea4d..73c80c77 100644
--- a/pydis_site/apps/api/tests/test_roles.py
+++ b/pydis_site/apps/api/tests/test_roles.py
@@ -1,7 +1,7 @@
from django.urls import reverse
from .base import AuthenticatedAPITestCase
-from ..models import Role
+from ..models import Role, User
class CreationTests(AuthenticatedAPITestCase):
@@ -35,6 +35,20 @@ class CreationTests(AuthenticatedAPITestCase):
permissions=6,
position=0,
)
+ cls.role_to_delete = Role.objects.create(
+ id=7,
+ name="role to delete",
+ colour=7,
+ permissions=7,
+ position=0,
+ )
+ cls.role_unassigned_test_user = User.objects.create(
+ id=8,
+ name="role_unassigned_test_user",
+ discriminator="0000",
+ roles=[cls.role_to_delete.id],
+ in_guild=True
+ )
def _validate_roledict(self, role_dict: dict) -> None:
"""Helper method to validate a dict representing a role."""
@@ -81,11 +95,11 @@ class CreationTests(AuthenticatedAPITestCase):
url = reverse('api:bot:role-list')
response = self.client.get(url)
- self.assertContains(response, text="id", count=4, status_code=200)
+ self.assertContains(response, text="id", count=5, status_code=200)
roles = response.json()
self.assertIsInstance(roles, list)
- self.assertEqual(len(roles), 4)
+ self.assertEqual(len(roles), 5)
for role in roles:
self._validate_roledict(role)
@@ -181,6 +195,12 @@ class CreationTests(AuthenticatedAPITestCase):
response = self.client.delete(url)
self.assertEqual(response.status_code, 204)
+ def test_role_delete_unassigned(self):
+ """Tests if the deleted Role gets unassigned from the user."""
+ self.role_to_delete.delete()
+ self.role_unassigned_test_user.refresh_from_db()
+ self.assertEqual(self.role_unassigned_test_user.roles, [])
+
def test_role_detail_404_all_methods(self):
"""Tests detail view with non-existing ID."""
url = reverse('api:bot:role-detail', args=(20190815,))
diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py
index 295bcf64..5d10069d 100644
--- a/pydis_site/apps/api/tests/test_users.py
+++ b/pydis_site/apps/api/tests/test_users.py
@@ -1,10 +1,10 @@
-from unittest.mock import patch
+import random
+from unittest.mock import Mock, patch
-from django.core.exceptions import ObjectDoesNotExist
from django.urls import reverse
from .base import AuthenticatedAPITestCase
-from ..models import Role, User
+from ..models import Infraction, Role, User
from ..models.bot.metricity import NotFoundError
from ..viewsets.bot.user import UserListPagination
@@ -421,10 +421,10 @@ class UserMetricityTests(AuthenticatedAPITestCase):
# Then
self.assertEqual(response.status_code, 200)
- self.assertEqual(response.json(), {
+ self.assertCountEqual(response.json(), {
"joined_at": joined_at,
"total_messages": total_messages,
- "voice_banned": False,
+ "voice_gate_blocked": False,
"activity_blocks": total_blocks
})
@@ -451,23 +451,36 @@ class UserMetricityTests(AuthenticatedAPITestCase):
self.assertEqual(response.status_code, 404)
def test_metricity_voice_banned(self):
+ queryset_with_values = Mock(spec=Infraction.objects)
+ queryset_with_values.filter.return_value = queryset_with_values
+ queryset_with_values.exists.return_value = True
+
+ queryset_without_values = Mock(spec=Infraction.objects)
+ queryset_without_values.filter.return_value = queryset_without_values
+ queryset_without_values.exists.return_value = False
cases = [
- {'exception': None, 'voice_banned': True},
- {'exception': ObjectDoesNotExist, 'voice_banned': False},
+ {'voice_infractions': queryset_with_values, 'voice_gate_blocked': True},
+ {'voice_infractions': queryset_without_values, 'voice_gate_blocked': False},
]
self.mock_metricity_user("foo", 1, 1, [["bar", 1]])
for case in cases:
- with self.subTest(exception=case['exception'], voice_banned=case['voice_banned']):
- with patch("pydis_site.apps.api.viewsets.bot.user.Infraction.objects.get") as p:
- p.side_effect = case['exception']
+ with self.subTest(
+ voice_infractions=case['voice_infractions'],
+ voice_gate_blocked=case['voice_gate_blocked']
+ ):
+ with patch("pydis_site.apps.api.viewsets.bot.user.Infraction.objects.filter") as p:
+ p.return_value = case['voice_infractions']
url = reverse('api:bot:user-metricity-data', args=[0])
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
- self.assertEqual(response.json()["voice_banned"], case["voice_banned"])
+ self.assertEqual(
+ response.json()["voice_gate_blocked"],
+ case["voice_gate_blocked"]
+ )
def test_metricity_review_data(self):
# Given
@@ -508,3 +521,45 @@ class UserMetricityTests(AuthenticatedAPITestCase):
self.metricity.total_messages.side_effect = NotFoundError()
self.metricity.total_message_blocks.side_effect = NotFoundError()
self.metricity.top_channel_activity.side_effect = NotFoundError()
+
+
+class UserViewSetTests(AuthenticatedAPITestCase):
+ @classmethod
+ def setUpTestData(cls):
+ cls.searched_user = User.objects.create(
+ id=12095219,
+ name=f"Test user {random.randint(100, 1000)}",
+ discriminator=random.randint(1, 9999),
+ in_guild=True,
+ )
+ cls.other_user = User.objects.create(
+ id=18259125,
+ name=f"Test user {random.randint(100, 1000)}",
+ discriminator=random.randint(1, 9999),
+ in_guild=True,
+ )
+
+ def test_search_lookup_of_wanted_user(self) -> None:
+ """Searching a user by name and discriminator should return that user."""
+ url = reverse('api:bot:user-list')
+ params = {
+ 'username': self.searched_user.name,
+ 'discriminator': self.searched_user.discriminator,
+ }
+ response = self.client.get(url, params)
+ result = response.json()
+ self.assertEqual(result['count'], 1)
+ [user] = result['results']
+ self.assertEqual(user['id'], self.searched_user.id)
+
+ def test_search_lookup_of_unknown_user(self) -> None:
+ """Searching an unknown user should return no results."""
+ url = reverse('api:bot:user-list')
+ params = {
+ 'username': "f-string enjoyer",
+ 'discriminator': 1245,
+ }
+ response = self.client.get(url, params)
+ result = response.json()
+ self.assertEqual(result['count'], 0)
+ self.assertEqual(result['results'], [])
diff --git a/pydis_site/apps/api/viewsets/bot/filter_list.py b/pydis_site/apps/api/viewsets/bot/filter_list.py
index 2cb21ab9..4b05acee 100644
--- a/pydis_site/apps/api/viewsets/bot/filter_list.py
+++ b/pydis_site/apps/api/viewsets/bot/filter_list.py
@@ -59,7 +59,8 @@ class FilterListViewSet(ModelViewSet):
... ["GUILD_INVITE","Guild Invite"],
... ["FILE_FORMAT","File Format"],
... ["DOMAIN_NAME","Domain Name"],
- ... ["FILTER_TOKEN","Filter Token"]
+ ... ["FILTER_TOKEN","Filter Token"],
+ ... ["REDIRECT", "Redirect"]
... ]
#### Status codes
diff --git a/pydis_site/apps/api/viewsets/bot/infraction.py b/pydis_site/apps/api/viewsets/bot/infraction.py
index f8b0cb9d..8a48ed1f 100644
--- a/pydis_site/apps/api/viewsets/bot/infraction.py
+++ b/pydis_site/apps/api/viewsets/bot/infraction.py
@@ -70,7 +70,8 @@ class InfractionViewSet(
... 'actor': 125435062127820800,
... 'type': 'ban',
... 'reason': 'He terk my jerb!',
- ... 'hidden': True
+ ... 'hidden': True,
+ ... 'dm_sent': True
... }
... ]
@@ -100,7 +101,8 @@ class InfractionViewSet(
... 'hidden': True,
... 'type': 'ban',
... 'reason': 'He terk my jerb!',
- ... 'user': 172395097705414656
+ ... 'user': 172395097705414656,
+ ... 'dm_sent': False
... }
#### Response format
@@ -118,7 +120,8 @@ class InfractionViewSet(
>>> {
... 'active': True,
... 'expires_at': '4143-02-15T21:04:31+00:00',
- ... 'reason': 'durka derr'
+ ... 'reason': 'durka derr',
+ ... 'dm_sent': True
... }
#### Response format
diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py
index 922e6555..d0519e86 100644
--- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py
+++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py
@@ -1,18 +1,17 @@
from django.db.models import Case, Value, When
from django.db.models.query import QuerySet
-from django.http.request import HttpRequest
from django.shortcuts import get_object_or_404
from rest_framework.exceptions import ParseError
-from rest_framework.mixins import DestroyModelMixin
+from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.status import HTTP_201_CREATED
-from rest_framework.viewsets import ViewSet
+from rest_framework.viewsets import ModelViewSet
from pydis_site.apps.api.models.bot.off_topic_channel_name import OffTopicChannelName
from pydis_site.apps.api.serializers import OffTopicChannelNameSerializer
-class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet):
+class OffTopicChannelNameViewSet(ModelViewSet):
"""
View of off-topic channel names used by the bot to rotate our off-topic names on a daily basis.
@@ -58,6 +57,7 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet):
lookup_field = 'name'
serializer_class = OffTopicChannelNameSerializer
+ queryset = OffTopicChannelName.objects.all()
def get_object(self) -> OffTopicChannelName:
"""
@@ -65,15 +65,14 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet):
If it doesn't, a HTTP 404 is returned by way of throwing an exception.
"""
- queryset = self.get_queryset()
name = self.kwargs[self.lookup_field]
- return get_object_or_404(queryset, name=name)
+ return get_object_or_404(self.queryset, name=name)
def get_queryset(self) -> QuerySet:
"""Returns a queryset that covers the entire OffTopicChannelName table."""
return OffTopicChannelName.objects.all()
- def create(self, request: HttpRequest) -> Response:
+ def create(self, request: Request, *args, **kwargs) -> Response:
"""
DRF method for creating a new OffTopicChannelName.
@@ -91,7 +90,7 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet):
'name': ["This query parameter is required."]
})
- def list(self, request: HttpRequest) -> Response:
+ def list(self, request: Request, *args, **kwargs) -> Response:
"""
DRF method for listing OffTopicChannelName entries.
@@ -109,13 +108,13 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet):
'random_items': ["Must be a positive integer."]
})
- queryset = self.get_queryset().order_by('used', '?')[:random_count]
+ queryset = self.queryset.filter(active=True).order_by('used', '?')[:random_count]
# When any name is used in our listing then this means we reached end of round
# and we need to reset all other names `used` to False
if any(offtopic_name.used for offtopic_name in queryset):
# These names that we just got have to be excluded from updating used to False
- self.get_queryset().update(
+ self.queryset.update(
used=Case(
When(
name__in=(offtopic_name.name for offtopic_name in queryset),
@@ -126,13 +125,17 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet):
)
else:
# Otherwise mark selected names `used` to True
- self.get_queryset().filter(
+ self.queryset.filter(
name__in=(offtopic_name.name for offtopic_name in queryset)
).update(used=True)
serialized = self.serializer_class(queryset, many=True)
return Response(serialized.data)
- queryset = self.get_queryset()
+ params = {}
+ if active_param := request.query_params.get("active"):
+ params["active"] = active_param.lower() == "true"
+
+ queryset = self.queryset.filter(**params)
serialized = self.serializer_class(queryset, many=True)
return Response(serialized.data)
diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py
index 22d13dc4..3318b2b9 100644
--- a/pydis_site/apps/api/viewsets/bot/user.py
+++ b/pydis_site/apps/api/viewsets/bot/user.py
@@ -1,7 +1,8 @@
import typing
from collections import OrderedDict
-from django.core.exceptions import ObjectDoesNotExist
+from django.db.models import Q
+from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.pagination import PageNumberPagination
@@ -77,6 +78,8 @@ class UserViewSet(ModelViewSet):
... }
#### Optional Query Parameters
+ - username: username to search for
+ - discriminator: discriminator to search for
- page_size: number of Users in one page, defaults to 10,000
- page: page number
@@ -233,6 +236,8 @@ class UserViewSet(ModelViewSet):
serializer_class = UserSerializer
queryset = User.objects.all().order_by("id")
pagination_class = UserListPagination
+ filter_backends = (DjangoFilterBackend,)
+ filter_fields = ('name', 'discriminator')
def get_serializer(self, *args, **kwargs) -> ModelSerializer:
"""Set Serializer many attribute to True if request body contains a list."""
@@ -261,19 +266,19 @@ class UserViewSet(ModelViewSet):
"""Request handler for metricity_data endpoint."""
user = self.get_object()
- try:
- Infraction.objects.get(user__id=user.id, active=True, type="voice_ban")
- except ObjectDoesNotExist:
- voice_banned = False
- else:
- voice_banned = True
+ has_voice_infraction = Infraction.objects.filter(
+ Q(user__id=user.id, active=True),
+ Q(type="voice_ban") | Q(type="voice_mute")
+ ).exists()
with Metricity() as metricity:
try:
data = metricity.user(user.id)
+
data["total_messages"] = metricity.total_messages(user.id)
- data["voice_banned"] = voice_banned
data["activity_blocks"] = metricity.total_message_blocks(user.id)
+
+ data["voice_gate_blocked"] = has_voice_infraction
return Response(data, status=status.HTTP_200_OK)
except NotFoundError:
return Response(dict(detail="User not found in metricity"),
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing.md
index d367dbc7..4013962c 100644
--- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing.md
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing.md
@@ -27,7 +27,7 @@ Our projects on Python Discord are open source and [available on Github](https:/
</div>
</div>
<div class="card-footer">
- <a href="https://github.com/python-discord/sir-lancebot/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc" class="card-footer-item"><i class="far fa-exclamation-circle"></i>&ensp;Issues</a>
+ <a href="https://github.com/python-discord/sir-lancebot/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc" class="card-footer-item"><i class="fas fa-exclamation-circle"></i>&ensp;Issues</a>
<a href="https://github.com/python-discord/sir-lancebot/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc" class="card-footer-item"><i class="fas fa-code-merge"></i>&ensp;PRs</a>
</div>
<div class="card-footer">
@@ -54,7 +54,7 @@ Our projects on Python Discord are open source and [available on Github](https:/
</div>
</div>
<div class="card-footer">
- <a href="https://github.com/python-discord/bot/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc" class="card-footer-item"><i class="far fa-exclamation-circle"></i>&ensp;Issues</a>
+ <a href="https://github.com/python-discord/bot/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc" class="card-footer-item"><i class="fas fa-exclamation-circle"></i>&ensp;Issues</a>
<a href="https://github.com/python-discord/bot/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc" class="card-footer-item"><i class="fas fa-code-merge"></i>&ensp;PRs</a>
</div>
<div class="card-footer">
@@ -81,7 +81,7 @@ Our projects on Python Discord are open source and [available on Github](https:/
</div>
</div>
<div class="card-footer">
- <a href="https://github.com/python-discord/site/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc" class="card-footer-item"><i class="far fa-exclamation-circle"></i>&ensp;Issues</a>
+ <a href="https://github.com/python-discord/site/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc" class="card-footer-item"><i class="fas fa-exclamation-circle"></i>&ensp;Issues</a>
<a href="https://github.com/python-discord/site/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc" class="card-footer-item"><i class="fas fa-code-merge"></i>&ensp;PRs</a>
</div>
<div class="card-footer">
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/bot.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/bot.md
index b9589def..2aa10aa3 100644
--- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/bot.md
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/bot.md
@@ -14,10 +14,10 @@ First things first, to run the bot's code and make changes to it, you need a loc
<button type="button" class="card-header collapsible">
<span class="card-header-title subtitle is-6 my-2 ml-2">Getting started with Git and GitHub</span>
<span class="card-header-icon">
- <i class="fas fa-angle-down title is-5" aria-hidden="true"></i>
+ <i class="fas fa-fw fa-angle-down title is-5" aria-hidden="true"></i>
</span>
</button>
- <div class="collapsible-content">
+ <div class="collapsible-content collapsed">
<div class="card-content">
<p>If you don't have Git on your computer already, <a href="https://git-scm.com/downloads">install it</a>. You can additionally install a Git GUI such as <a href="https://www.gitkraken.com/download">GitKraken</a>, or the <a href="https://cli.github.com/manual/installation">GitHub CLI</a>.</p>
<p>To learn more about Git, you can look into <a href="../working-with-git">our guides</a>, as well as <a href="https://education.github.com/git-cheat-sheet-education.pdf">this cheatsheet</a>, <a href="https://learngitbranching.js.org">Learn Git Branching</a>, and otherwise any guide you can find on the internet. Once you got the basic idea though, the best way to learn Git is to use it.</p>
@@ -78,10 +78,10 @@ See [here](../obtaining-discord-ids) for help with obtaining Discord IDs.
<button type="button" class="card-header collapsible">
<span class="card-header-title subtitle is-6 my-2 ml-2">Optional config.yml</span>
<span class="card-header-icon">
- <i class="fas fa-angle-down title is-5" aria-hidden="true"></i>
+ <i class="fas fa-fw fa-angle-down title is-5" aria-hidden="true"></i>
</span>
</button>
- <div class="collapsible-content">
+ <div class="collapsible-content collapsed">
<div class="card-content">
<p>If you used the provided server template, and you're not sure which channels belong where in the config file, you can use the config below. Pay attention to the comments with several <code>#</code> symbols, and replace the <code>�</code> characters with the right IDs.</p>
<pre>
@@ -196,6 +196,7 @@ guild:
big_brother: �
dev_log: �
duck_pond: �
+ incidents: �
incidents_archive: �
python_news: &PYNEWS_WEBHOOK �
talent_pool: �
@@ -350,7 +351,7 @@ style:
trashcan: "<:trashcan:�>"
-##### << Optional - If you don't care about the filtering and help channel cogs, ignore the rest of this file >> #####
+##### << Optional - If you don't care about the filtering, help channel and py-news cogs, ignore the rest of this file >> #####
filter:
# What do we filter?
filter_domains: true
@@ -426,6 +427,10 @@ help_channels:
notify_roles:
- *HELPERS_ROLE
+python_news:
+ channel: *DEV_PY_NEWS
+ webhook: *PYNEWS_WEBHOOK
+
##### << Add any additional sections you need to override from config-default.yml >> #####
</code>
</pre>
@@ -453,10 +458,10 @@ We understand this is tedious and are working on a better solution for setting u
<button type="button" class="card-header collapsible">
<span class="card-header-title subtitle is-6 my-2 ml-2">Why do you need a separate config file?</span>
<span class="card-header-icon">
- <i class="fas fa-angle-down title is-5" aria-hidden="true"></i>
+ <i class="fas fa-fw fa-angle-down title is-5" aria-hidden="true"></i>
</span>
</button>
- <div class="collapsible-content">
+ <div class="collapsible-content collapsed">
<div class="card-content">
While it's technically possible to edit <code>config-default.yml</code> to match your server, it is heavily discouraged.
This file's purpose is to provide the configurations the Python bot needs to run in the Python server in production, and should remain as such.
@@ -482,10 +487,10 @@ You are now almost ready to run the Python bot. The simplest way to do so is wit
<button type="button" class="card-header collapsible">
<span class="card-header-title subtitle is-6 my-2 ml-2">Getting started with Docker</span>
<span class="card-header-icon">
- <i class="fas fa-angle-down title is-5" aria-hidden="true"></i>
+ <i class="fas fa-fw fa-angle-down title is-5" aria-hidden="true"></i>
</span>
</button>
- <div class="collapsible-content">
+ <div class="collapsible-content collapsed">
<div class="card-content">
The requirements for Docker are:
<ul>
@@ -536,10 +541,10 @@ With at least the site running in Docker already (see the previous section on ho
<button type="button" class="card-header collapsible">
<span class="card-header-title subtitle is-6 my-2 ml-2">Ways to run code</span>
<span class="card-header-icon">
- <i class="fas fa-angle-down title is-5" aria-hidden="true"></i>
+ <i class="fas fa-fw fa-angle-down title is-5" aria-hidden="true"></i>
</span>
</button>
- <div class="collapsible-content">
+ <div class="collapsible-content collapsed">
<div class="card-content">
Notice that the bot is started as a module. There are several ways to do so:
<ul>
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/setting-test-server-and-bot-account.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/setting-test-server-and-bot-account.md
index c14fe50d..43d1c8f5 100644
--- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/setting-test-server-and-bot-account.md
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/setting-test-server-and-bot-account.md
@@ -18,13 +18,11 @@ icon: fab fa-discord
4. Change your bot's `Public Bot` setting off so only you can invite it, save, and then get your **Bot Token** with the `Copy` button.
> **Note:** **DO NOT** post your bot token anywhere public, or it can and will be compromised.
5. Save your **Bot Token** somewhere safe to use in the project settings later.
-6. In the `General Information` tab, grab the **Client ID**.
+6. In the `OAuth2` tab, grab the **Client ID**.
7. Replace `<CLIENT_ID_HERE>` in the following URL and visit it in the browser to invite your bot to your new test server.
```plaintext
https://discordapp.com/api/oauth2/authorize?client_id=<CLIENT_ID_HERE>&permissions=8&scope=bot
```
-Optionally, you can generate your own invite url in the `OAuth` tab, after selecting `bot` as the scope.
-
---
## Obtain the IDs
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot.md
index a0d3d463..e3cd8f0c 100644
--- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot.md
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot.md
@@ -18,12 +18,17 @@ toc: 1
## Using Gitpod
Sir Lancebot can be edited and tested on Gitpod. Gitpod will automatically install the correct dependencies and Python version, so you can get straight to coding.
-To do this, you will need a Gitpod account, which you can get [here](https://www.gitpod.io/#get-started), and a fork of Sir Lancebot. This guide covers forking the repository [here](#fork-the-project). Afterwards, either click the button on Sir Lancebot's README or go to [https://gitpod.io/#/python-discord/sir-lancebot]() and run the following commands in the terminal:
+
+To do this, you will need a Gitpod account, which you can get [here](https://www.gitpod.io/#get-started), and a fork of Sir Lancebot. This guide covers forking the repository [here](#fork-the-project).
+
+Afterwards, click on [this link](https://gitpod.io/#/github.com/python-discord/sir-lancebot) to spin up a new workspace for Sir Lancebot. Then run the following commands in the terminal after the existing tasks have finished running:
```sh
git remote rename origin upstream
git add remote origin https://github.com/{your_username}/sir-lancebot
```
-Make sure you replace `{your_username}` with your Github username. These commands will set Python Discord as the parent repository, and your branch as the fork. This means you can easily grab new changes from the parent repository. Once you set your environment variables to test your code, you are ready to begin contributing to Sir Lancebot.
+Make sure you replace `{your_username}` with your Github username. These commands will set the Sir Lancebot repository as the secondary remote, and your fork as the primary remote. This means you can easily grab new changes from the main Sir Lancebot repository.
+
+Once you've set up [a test server and bot account](#test-server-and-bot-account) and your [environment variables](#environment-variables), you are ready to begin contributing to Sir Lancebot!
## Using Docker
Sir Lancebot can be started using Docker. Using Docker is generally recommended (but not strictly required) because it abstracts away some additional set up work.
diff --git a/pydis_site/apps/content/resources/rules.md b/pydis_site/apps/content/resources/rules.md
index ef6cc4d1..b788c81b 100644
--- a/pydis_site/apps/content/resources/rules.md
+++ b/pydis_site/apps/content/resources/rules.md
@@ -10,21 +10,21 @@ We have a small but strict set of rules on our server. Please read over them and
> 3. Respect staff members and listen to their instructions.
> 4. Use English to the best of your ability. Be polite if someone speaks English imperfectly.
> 5. Do not provide or request help on projects that may break laws, breach terms of services, or are malicious or inappropriate.
-> 6. Do not post unapproved advertising.
-> 7. Keep discussions relevant to the channel topic. Each channel's description tells you the topic.
+> 6. Do not post unapproved advertising.
+> 7. Keep discussions relevant to the channel topic. Each channel's description tells you the topic.
> 8. Do not help with ongoing exams. When helping with homework, help people learn how to do the assignment without doing it for them.
> 9. Do not offer or ask for paid work of any kind.
-# Nickname Policy
+# Name & Profile Policy
-In order to keep things pleasant and workable for both users and staff members, we enforce the following requirements regarding your nickname.
+In order to keep things pleasant and workable for both users and staff members, we enforce the following requirements regarding your name, avatar, and profile. Staff reserve the right to change any nickname we judge to be violating these requirements.
-1. No blank or "invisible" names
-2. No slurs or other offensive sentiments
-3. No noisy unicode characters - for example, z̯̯͡a̧͎̺̻̝͕̠l̡͓̫̣g̹̲o̡̼̘ or byte order marks
-4. No nicknames designed to annoy other users
+We also reserve the right to enforce compliance of hateful or otherwise inappropriate usernames and profiles regardless of the server-specific nickname or profile.
+​
-Staff reserves the right to change the nickname of any user for any reason. Failure to comply with these requirements may result in you losing the right to change your nickname. We also reserve the right to discipline users with offensive usernames, regardless of the nickname they're using.
+1. No blank or "invisible" names.
+2. No slurs or other offensive sentiments or imagery.
+3. No noisy unicode characters (for example z̯̯͡a̧͎̺̻̝͕̠l̡͓̫̣g̹̲o̡̼̘) or rapidly flashing avatars.
# Infractions
diff --git a/pydis_site/apps/content/urls.py b/pydis_site/apps/content/urls.py
index fe7c2852..f8496095 100644
--- a/pydis_site/apps/content/urls.py
+++ b/pydis_site/apps/content/urls.py
@@ -30,7 +30,7 @@ def __get_all_files(root: Path, folder: typing.Optional[Path] = None) -> list[st
def get_all_pages() -> typing.Iterator[dict[str, str]]:
- """Yield a dict of all pag categories."""
+ """Yield a dict of all page categories."""
for location in __get_all_files(Path("pydis_site", "apps", "content", "resources")):
yield {"location": location}
diff --git a/pydis_site/apps/redirect/redirects.yaml b/pydis_site/apps/redirect/redirects.yaml
index def4154b..4a48ba0c 100644
--- a/pydis_site/apps/redirect/redirects.yaml
+++ b/pydis_site/apps/redirect/redirects.yaml
@@ -83,13 +83,54 @@ good_questions_redirect_alt:
redirect_arguments: ["guides/pydis-guides/asking-good-questions"]
# Resources
+resources_old_communities_redirect:
+ original_path: pages/resources/communities/
+ redirect_route: "resources:index"
+ redirect_arguments: ["community"]
+
resources_index_redirect:
original_path: pages/resources/
redirect_route: "resources:index"
-resources_resources_redirect:
- original_path: pages/resources/<str:category>/
- redirect_route: "resources:resources"
+resources_reading_redirect:
+ original_path: resources/reading/
+ redirect_route: "resources:index"
+ redirect_arguments: ["book"]
+
+resources_books_redirect:
+ original_path: resources/books/
+ redirect_route: "resources:index"
+ redirect_arguments: ["book"]
+
+resources_videos_redirect:
+ original_path: resources/videos/
+ redirect_route: "resources:index"
+ redirect_arguments: ["video"]
+
+resources_courses_redirect:
+ original_path: resources/courses/
+ redirect_route: "resources:index"
+ redirect_arguments: ["course"]
+
+resources_communities_redirect:
+ original_path: resources/communities/
+ redirect_route: "resources:index"
+ redirect_arguments: ["community"]
+
+resources_podcasts_redirect:
+ original_path: resources/podcasts/
+ redirect_route: "resources:index"
+ redirect_arguments: ["podcast"]
+
+resources_tutorials_redirect:
+ original_path: resources/tutorials/
+ redirect_route: "resources:index"
+ redirect_arguments: ["tutorial"]
+
+resources_tools_redirect:
+ original_path: resources/tools/
+ redirect_route: "resources:index"
+ redirect_arguments: ["tool"]
# Events
events_index_redirect:
@@ -182,7 +223,7 @@ events_game_jams_twenty_twenty_rules_redirect:
redirect_arguments: ["game-jams/2020/rules"]
events_game_jams_twenty_twenty_technical_requirements_redirect:
- original_path: pages/events/game-jam-2020/technical-requirements
+ original_path: pages/events/game-jam-2020/technical-requirements/
redirect_route: "events:page"
redirect_arguments: ["game-jams/2020/technical-requirements"]
diff --git a/pydis_site/apps/redirect/urls.py b/pydis_site/apps/redirect/urls.py
index 6187af17..f7ddf45b 100644
--- a/pydis_site/apps/redirect/urls.py
+++ b/pydis_site/apps/redirect/urls.py
@@ -1,19 +1,105 @@
+import dataclasses
+import re
+
import yaml
-from django.conf import settings
-from django.urls import path
+from django import conf
+from django.urls import URLPattern, path
+from django_distill import distill_path
+from pydis_site import settings
+from pydis_site.apps.content import urls as pages_urls
from pydis_site.apps.redirect.views import CustomRedirectView
+from pydis_site.apps.resources import urls as resources_urls
app_name = "redirect"
-urlpatterns = [
- path(
- data["original_path"],
- CustomRedirectView.as_view(
- pattern_name=data["redirect_route"],
- static_args=tuple(data.get("redirect_arguments", ())),
- prefix_redirect=data.get("prefix_redirect", False)
- ),
- name=name
- )
- for name, data in yaml.safe_load(settings.REDIRECTIONS_PATH.read_text()).items()
-]
+
+
+__PARAMETER_REGEX = re.compile(r"<\w+:\w+>")
+REDIRECT_TEMPLATE = "<meta http-equiv=\"refresh\" content=\"0; URL={url}\"/>"
+
+
[email protected](frozen=True)
+class Redirect:
+ """Metadata about a redirect route."""
+
+ original_path: str
+ redirect_route: str
+ redirect_arguments: tuple[str] = tuple()
+
+ prefix_redirect: bool = False
+
+
+def map_redirect(name: str, data: Redirect) -> list[URLPattern]:
+ """Return a pattern using the Redirects app, or a static HTML redirect for static builds."""
+ if not settings.env("STATIC_BUILD"):
+ # Normal dynamic redirect
+ return [path(
+ data.original_path,
+ CustomRedirectView.as_view(
+ pattern_name=data.redirect_route,
+ static_args=tuple(data.redirect_arguments),
+ prefix_redirect=data.prefix_redirect
+ ),
+ name=name
+ )]
+
+ # Create static HTML redirects for static builds
+ new_app_name = data.redirect_route.split(":")[0]
+
+ if __PARAMETER_REGEX.search(data.original_path):
+ # Redirects for paths which accept parameters
+ # We generate an HTML redirect file for all possible entries
+ paths = []
+
+ class RedirectFunc:
+ def __init__(self, new_url: str, _name: str):
+ self.result = REDIRECT_TEMPLATE.format(url=new_url)
+ self.__qualname__ = _name
+
+ def __call__(self, *args, **kwargs):
+ return self.result
+
+ if new_app_name == resources_urls.app_name:
+ items = resources_urls.get_all_resources()
+ elif new_app_name == pages_urls.app_name:
+ items = pages_urls.get_all_pages()
+ else:
+ raise ValueError(f"Unknown app in redirect: {new_app_name}")
+
+ for item in items:
+ entry = list(item.values())[0]
+
+ # Replace dynamic redirect with concrete path
+ concrete_path = __PARAMETER_REGEX.sub(entry, data.original_path)
+ new_redirect = f"/{new_app_name}/{entry}"
+ pattern_name = f"{name}_{entry}"
+
+ paths.append(distill_path(
+ concrete_path,
+ RedirectFunc(new_redirect, pattern_name),
+ name=pattern_name
+ ))
+
+ return paths
+
+ else:
+ redirect_path_name = "pages" if new_app_name == "content" else new_app_name
+ if len(data.redirect_arguments) > 0:
+ redirect_arg = data.redirect_arguments[0]
+ else:
+ redirect_arg = "resources/"
+ new_redirect = f"/{redirect_path_name}/{redirect_arg}"
+
+ if new_redirect == "/resources/resources/":
+ new_redirect = "/resources/"
+
+ return [distill_path(
+ data.original_path,
+ lambda *args: REDIRECT_TEMPLATE.format(url=new_redirect),
+ name=name,
+ )]
+
+
+urlpatterns = []
+for _name, _data in yaml.safe_load(conf.settings.REDIRECTIONS_PATH.read_text()).items():
+ urlpatterns.extend(map_redirect(_name, Redirect(**_data)))
diff --git a/pydis_site/apps/resources/resources/communities/adafruit.yaml b/pydis_site/apps/resources/resources/adafruit.yaml
index e5c81a6c..c687f507 100644
--- a/pydis_site/apps/resources/resources/communities/adafruit.yaml
+++ b/pydis_site/apps/resources/resources/adafruit.yaml
@@ -1,15 +1,22 @@
+name: Adafruit
description: Adafruit is an open-source electronics manufacturer
that makes all the components you need to start your own Python-powered hardware projects.
Their official community host regular show-and-tells,
provide help with your projects,
and the Adafruit devs do all the CircuitPython Development right out in the open.
title_image: https://www.mouser.com/images/suppliers/logos/adafruit.png
-title_url: https://discord.gg/adafruit
-position: 4
+title_url: https://adafruit.com/
urls:
-- icon: branding/discord
- url: https://discord.gg/adafruit
- color: blurple
-- icon: regular/link
- url: https://adafruit.com/
- color: teal
+ - icon: branding/discord
+ url: https://discord.gg/adafruit
+ color: blurple
+tags:
+ topics:
+ - microcontrollers
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - community
diff --git a/pydis_site/apps/resources/resources/tools/editors/atom.yaml b/pydis_site/apps/resources/resources/atom.yaml
index c44f9b5b..26e125b1 100644
--- a/pydis_site/apps/resources/resources/tools/editors/atom.yaml
+++ b/pydis_site/apps/resources/resources/atom.yaml
@@ -2,4 +2,13 @@ description: A free Electron-based editor, a "hackable text editor for the 21st
by the GitHub team.
name: Atom
title_url: https://atom.io/
-position: 0
+tags:
+ topics:
+ - general
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - tool
diff --git a/pydis_site/apps/resources/resources/reading/books/automate_the_boring_stuff.yaml b/pydis_site/apps/resources/resources/automate_the_boring_stuff_book.yaml
index 3812029c..63f63193 100644
--- a/pydis_site/apps/resources/resources/reading/books/automate_the_boring_stuff.yaml
+++ b/pydis_site/apps/resources/resources/automate_the_boring_stuff_book.yaml
@@ -4,11 +4,18 @@ description: One of the best books out there for Python beginners. This book wil
the web, manipulating files and automating keyboard and mouse input. Ideal for an
office worker who wants to make himself more useful.
name: Automate the Boring Stuff with Python
-position: 2
+title_url: https://automatetheboringstuff.com/
urls:
-- icon: regular/book
- url: https://automatetheboringstuff.com/
+- icon: branding/goodreads
+ url: https://www.goodreads.com/book/show/22514127-automate-the-boring-stuff-with-python
color: black
-- icon: branding/amazon
- url: https://www.amazon.com/Automate-Boring-Stuff-Python-Programming/dp/1593275994/
- color: amazon-orange
+tags:
+ topics:
+ - general
+ payment_tiers:
+ - free
+ - paid
+ difficulty:
+ - beginner
+ type:
+ - book
diff --git a/pydis_site/apps/resources/resources/automate_the_boring_stuff_course.yaml b/pydis_site/apps/resources/resources/automate_the_boring_stuff_course.yaml
new file mode 100644
index 00000000..4632f5bd
--- /dev/null
+++ b/pydis_site/apps/resources/resources/automate_the_boring_stuff_course.yaml
@@ -0,0 +1,13 @@
+description: The interactive course version of Al Sweigart's excellent book for beginners, taught by the author himself.
+name: Automate the Boring Stuff with Python Udemy Course
+title_url: https://www.udemy.com/automate/
+tags:
+ topics:
+ - general
+ payment_tiers:
+ - paid
+ difficulty:
+ - beginner
+ type:
+ - course
+ - interactive
diff --git a/pydis_site/apps/resources/resources/communities/awesome_programming_discord.yaml b/pydis_site/apps/resources/resources/awesome_programming_discord.yaml
index 335ac507..0ef7aefc 100644
--- a/pydis_site/apps/resources/resources/communities/awesome_programming_discord.yaml
+++ b/pydis_site/apps/resources/resources/awesome_programming_discord.yaml
@@ -6,4 +6,13 @@ title_icon: branding/github
title_icon_color: black
title_url: https://github.com/mhxion/awesome-programming-discord
name: awesome-programming-discord
-position: 10
+tags:
+ topics:
+ - general
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - community
diff --git a/pydis_site/apps/resources/resources/reading/books/byte_of_python.yaml b/pydis_site/apps/resources/resources/byte_of_python.yaml
index 2530c1a4..c2f6ab84 100644
--- a/pydis_site/apps/resources/resources/reading/books/byte_of_python.yaml
+++ b/pydis_site/apps/resources/resources/byte_of_python.yaml
@@ -2,14 +2,21 @@ description: A free book on programming using the Python language.
It serves as a tutorial or guide to the Python language for a beginner audience.
If all you know about computers is how to save text files, then this is the book for you.
name: A Byte of Python
-position: 1
+title_url: https://python.swaroopch.com/
urls:
-- icon: regular/link
- url: https://python.swaroopch.com/
- color: teal
- icon: regular/book
url: https://www.lulu.com/shop/swaroop-c-h/a-byte-of-python/paperback/product-21142968.html
color: black
-- icon: branding/amazon
- url: https://www.amazon.com/Byte-Python-Swaroop-C-H-ebook/dp/B00FJ7S2JU/
- color: amazon-orange
+- icon: branding/goodreads
+ url: https://www.goodreads.com/book/show/6762544-a-byte-of-python
+ color: black
+tags:
+ topics:
+ - general
+ payment_tiers:
+ - free
+ - paid
+ difficulty:
+ - beginner
+ type:
+ - book
diff --git a/pydis_site/apps/resources/resources/interactive/code_combat.yaml b/pydis_site/apps/resources/resources/code_combat.yaml
index 30f20c28..84597c4d 100644
--- a/pydis_site/apps/resources/resources/interactive/code_combat.yaml
+++ b/pydis_site/apps/resources/resources/code_combat.yaml
@@ -1,11 +1,20 @@
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
-position: 0
+title_url: https://codecombat.com/
urls:
-- icon: regular/link
- url: https://codecombat.com/
- color: teal
- icon: branding/github
url: https://github.com/codecombat/codecombat
color: black
+tags:
+ topics:
+ - general
+ - algorithms and data structures
+ payment_tiers:
+ - free
+ - subscription
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - interactive
diff --git a/pydis_site/apps/resources/resources/communities/_category_info.yaml b/pydis_site/apps/resources/resources/communities/_category_info.yaml
deleted file mode 100644
index b9cb6533..00000000
--- a/pydis_site/apps/resources/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/resources/resources/videos/corey_schafer.yaml b/pydis_site/apps/resources/resources/corey_schafer.yaml
index a7cca18a..d66ea004 100644
--- a/pydis_site/apps/resources/resources/videos/corey_schafer.yaml
+++ b/pydis_site/apps/resources/resources/corey_schafer.yaml
@@ -1,3 +1,4 @@
+name: Corey Schafer
description: 'Corey has a number of exceptionally high quality tutorial series
on everything from Python basics to Django and Flask:
<ul>
@@ -9,11 +10,21 @@ description: 'Corey has a number of exceptionally high quality tutorial series
Check out his channel for more video series!
'
title_image: https://i.imgur.com/KIfWw3b.png
-position: 0
+title_url: https://www.youtube.com/channel/UCCezIgC97PvUuR4_gbFUs5g
urls:
- - icon: branding/youtube
- url: https://www.youtube.com/channel/UCCezIgC97PvUuR4_gbFUs5g
- color: youtube-red
- - icon: regular/link
+ - icon: solid/external-link-alt
url: https://coreyms.com/
color: teal
+tags:
+ topics:
+ - general
+ - software design
+ - web development
+ - tooling
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - video
diff --git a/pydis_site/apps/resources/resources/courses/_category_info.yaml b/pydis_site/apps/resources/resources/courses/_category_info.yaml
deleted file mode 100644
index 948b48de..00000000
--- a/pydis_site/apps/resources/resources/courses/_category_info.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-description: Listing of best Python courses.
-name: Courses
-default_icon: regular/graduation-cap
-default_icon_color: black
diff --git a/pydis_site/apps/resources/resources/courses/automate_the_boring_stuff_with_python.yaml b/pydis_site/apps/resources/resources/courses/automate_the_boring_stuff_with_python.yaml
deleted file mode 100644
index 66034ea2..00000000
--- a/pydis_site/apps/resources/resources/courses/automate_the_boring_stuff_with_python.yaml
+++ /dev/null
@@ -1,5 +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
-title_url: https://www.udemy.com/automate/?couponCode=FOR_LIKE_10_BUCKS
-position: 3
diff --git a/pydis_site/apps/resources/resources/data_science_from_scratch.yaml b/pydis_site/apps/resources/resources/data_science_from_scratch.yaml
new file mode 100644
index 00000000..86955fdb
--- /dev/null
+++ b/pydis_site/apps/resources/resources/data_science_from_scratch.yaml
@@ -0,0 +1,22 @@
+description: Data Science from Scratch is a good introduction to data science for the complete beginner, and covers
+ some of the fundamentals of Python programming as well as the basic math, probability and statistics needed to get
+ started. While either edition of this book is useful for those with prior Python experience, complete beginners
+ should use the second edition, which contains more up-to-date code examples and better practices.
+name: Data Science from Scratch
+title_url: https://www.oreilly.com/library/view/data-science-from/9781492041122/
+urls:
+ - icon: branding/goodreads
+ url: https://www.goodreads.com/en/book/show/52059715-data-science-from-scratch
+ color: black
+ - icon: branding/github
+ url: https://github.com/joelgrus/data-science-from-scratch
+ color: black
+tags:
+ topics:
+ - data science
+ payment_tiers:
+ - paid
+ difficulty:
+ - beginner
+ type:
+ - book
diff --git a/pydis_site/apps/resources/resources/interactive/edublocks.yaml b/pydis_site/apps/resources/resources/edublocks.yaml
index 7c6ca02b..3eaefc35 100644
--- a/pydis_site/apps/resources/resources/interactive/edublocks.yaml
+++ b/pydis_site/apps/resources/resources/edublocks.yaml
@@ -7,4 +7,12 @@ description: EduBlocks provides a simple drag and drop interface to help beginne
and export the code to run on actual devices.
name: EduBlocks
title_url: https://edublocks.org/
-position: 5
+tags:
+ topics:
+ - general
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ type:
+ - interactive
diff --git a/pydis_site/apps/resources/resources/effective_python.yaml b/pydis_site/apps/resources/resources/effective_python.yaml
new file mode 100644
index 00000000..b82fa0c3
--- /dev/null
+++ b/pydis_site/apps/resources/resources/effective_python.yaml
@@ -0,0 +1,21 @@
+description: A book that gives 90 best practices for writing excellent Python. Great
+ for intermediates.
+name: Effective Python
+title_url: https://effectivepython.com/
+urls:
+- icon: branding/goodreads
+ url: https://www.goodreads.com/book/show/48566725-effective-python
+ color: black
+- icon: branding/github
+ url: https://github.com/bslatkin/effectivepython
+ color: black
+tags:
+ topics:
+ - general
+ - software design
+ payment_tiers:
+ - paid
+ difficulty:
+ - intermediate
+ type:
+ - book
diff --git a/pydis_site/apps/resources/resources/interactive/exercism.yaml b/pydis_site/apps/resources/resources/exercism.yaml
index 68b458d0..c623db2d 100644
--- a/pydis_site/apps/resources/resources/interactive/exercism.yaml
+++ b/pydis_site/apps/resources/resources/exercism.yaml
@@ -2,12 +2,19 @@ description: Level up your programming skills with more than 2600 exercises acro
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
-position: 1
+name: Exercism
+title_url: https://exercism.org/
urls:
-- icon: regular/link
- url: https://exercism.io/
- color: teal
- icon: branding/github
url: https://github.com/exercism/python
color: black
+tags:
+ topics:
+ - general
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - interactive
diff --git a/pydis_site/apps/resources/resources/flask_web_development.yaml b/pydis_site/apps/resources/resources/flask_web_development.yaml
new file mode 100644
index 00000000..6905b2b4
--- /dev/null
+++ b/pydis_site/apps/resources/resources/flask_web_development.yaml
@@ -0,0 +1,21 @@
+description: A comprehensive Flask walkthrough that has you building a complete social
+ blogging application from scratch.
+name: Flask Web Development
+title_url: http://shop.oreilly.com/product/0636920031116.do
+urls:
+- icon: branding/goodreads
+ url: https://www.goodreads.com/book/show/18774655-flask-web-development
+ color: black
+- icon: branding/github
+ url: https://github.com/miguelgrinberg/flasky
+ color: black
+tags:
+ topics:
+ - web development
+ payment_tiers:
+ - paid
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - book
diff --git a/pydis_site/apps/resources/resources/fluent_python.yaml b/pydis_site/apps/resources/resources/fluent_python.yaml
new file mode 100644
index 00000000..c22fd388
--- /dev/null
+++ b/pydis_site/apps/resources/resources/fluent_python.yaml
@@ -0,0 +1,21 @@
+description: A veritable tome of intermediate and advanced Python information. A must-read
+ for any Python professional. By far the most recommended book for intermediates.
+name: Fluent Python
+title_url: https://www.oreilly.com/library/view/fluent-python/9781491946237/
+urls:
+- icon: branding/goodreads
+ url: https://www.goodreads.com/book/show/22800567-fluent-python
+ color: black
+- icon: branding/github
+ url: https://github.com/fluentpython
+ color: black
+tags:
+ topics:
+ - general
+ - software design
+ payment_tiers:
+ - paid
+ difficulty:
+ - intermediate
+ type:
+ - book
diff --git a/pydis_site/apps/resources/resources/reading/tutorials/getting_started_with_kivy.yaml b/pydis_site/apps/resources/resources/getting_started_with_kivy.yaml
index d1d9a7d2..06eb2c14 100644
--- a/pydis_site/apps/resources/resources/reading/tutorials/getting_started_with_kivy.yaml
+++ b/pydis_site/apps/resources/resources/getting_started_with_kivy.yaml
@@ -2,4 +2,13 @@ description: A big list of excellent resources for getting started making Kivy a
name: Getting Started with Kivy
title_url: https://blog.kivy.org/2019/12/getting-started-with-kivy/
icon_image: https://raw.githubusercontent.com/kivy/kivy-website/master/logos/kivy-logo-black-256.png
-position: 3
+tags:
+ topics:
+ - user interface
+ - game development
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ type:
+ - tutorial
diff --git a/pydis_site/apps/resources/resources/reading/tutorials/getting_started_with_python_for_non_programmers.yaml b/pydis_site/apps/resources/resources/getting_started_with_python_for_non_programmers.yaml
index 3250a7c4..6fab0114 100644
--- a/pydis_site/apps/resources/resources/reading/tutorials/getting_started_with_python_for_non_programmers.yaml
+++ b/pydis_site/apps/resources/resources/getting_started_with_python_for_non_programmers.yaml
@@ -2,4 +2,12 @@ description: A list of beginner resources for programmers with no prior develope
from Python's official guide.
name: Getting Started with Python for Non-Programmers
title_url: https://wiki.python.org/moin/BeginnersGuide/NonProgrammers
-position: 1
+tags:
+ topics:
+ - general
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ type:
+ - tutorial
diff --git a/pydis_site/apps/resources/resources/reading/tutorials/getting_started_with_python_for_programmers.yaml b/pydis_site/apps/resources/resources/getting_started_with_python_for_programmers.yaml
index b65e0e12..74b6efb9 100644
--- a/pydis_site/apps/resources/resources/reading/tutorials/getting_started_with_python_for_programmers.yaml
+++ b/pydis_site/apps/resources/resources/getting_started_with_python_for_programmers.yaml
@@ -3,3 +3,12 @@ description: A list of beginner resources for programmers coming from other lang
name: Getting Started with Python for Programmers
title_url: https://wiki.python.org/moin/BeginnersGuide/Programmers
position: 0
+tags:
+ topics:
+ - general
+ payment_tiers:
+ - free
+ difficulty:
+ - intermediate
+ type:
+ - tutorial
diff --git a/pydis_site/apps/resources/resources/google_colab.yaml b/pydis_site/apps/resources/resources/google_colab.yaml
new file mode 100644
index 00000000..5e1ca677
--- /dev/null
+++ b/pydis_site/apps/resources/resources/google_colab.yaml
@@ -0,0 +1,17 @@
+description: Google Colab is a custom version of Jupyter Notebook that runs code in the cloud, allowing you to
+ share your Colab notebooks with other people and work collaboratively.
+ Colab offers a generous amount of memory and computation time for free, and allows you to run programs on GPUs,
+ making it a great deep learning sandbox for beginners.
+name: Google Colab
+title_url: https://colab.research.google.com/notebooks/intro.ipynb
+tags:
+ topics:
+ - general
+ - data science
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - tool
diff --git a/pydis_site/apps/resources/resources/hitchhikers_guide_to_python.yaml b/pydis_site/apps/resources/resources/hitchhikers_guide_to_python.yaml
new file mode 100644
index 00000000..e48e5717
--- /dev/null
+++ b/pydis_site/apps/resources/resources/hitchhikers_guide_to_python.yaml
@@ -0,0 +1,18 @@
+description: A best practice handbook for both novice and expert Python developers to the installation,
+ configuration, and usage of Python on a daily basis.
+name: The Hitchhiker's Guide to Python
+title_url: https://python-guide.org/
+urls:
+- icon: branding/goodreads
+ url: https://www.goodreads.com/book/show/28321007-the-hitchhiker-s-guide-to-python
+ color: black
+tags:
+ topics:
+ - general
+ payment_tiers:
+ - paid
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - book
diff --git a/pydis_site/apps/resources/resources/reading/books/inferential_thinking.yaml b/pydis_site/apps/resources/resources/inferential_thinking.yaml
index 27fad4f7..a8cf2bc8 100644
--- a/pydis_site/apps/resources/resources/reading/books/inferential_thinking.yaml
+++ b/pydis_site/apps/resources/resources/inferential_thinking.yaml
@@ -2,8 +2,14 @@ description: Inferential Thinking is the textbook for the <a href="http://data8.
It introduces you the fundamentals of both Data Science and Python at a level accessible to all.
It is available both through your browser and in PDF form.
name: Inferential Thinking
-position: 13
-urls:
- - icon: regular/link
- url: https://www.inferentialthinking.com/chapters/intro
- color: teal
+title_url: https://inferentialthinking.com/chapters/intro
+tags:
+ topics:
+ - data science
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - book
diff --git a/pydis_site/apps/resources/resources/interactive/_category_info.yaml b/pydis_site/apps/resources/resources/interactive/_category_info.yaml
deleted file mode 100644
index 7e8f34d9..00000000
--- a/pydis_site/apps/resources/resources/interactive/_category_info.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-description: Learn Python with interactive courses, games, and programming challenges.
-name: Interactive
-default_icon: branding/python
-default_icon_color: black
diff --git a/pydis_site/apps/resources/resources/interactive/jetbrains_academy.yaml b/pydis_site/apps/resources/resources/jetbrains_academy.yaml
index 937831fa..c3cb7657 100644
--- a/pydis_site/apps/resources/resources/interactive/jetbrains_academy.yaml
+++ b/pydis_site/apps/resources/resources/jetbrains_academy.yaml
@@ -5,4 +5,14 @@ description: Learn Python with a wide range of high quality, project-based lesso
It requires a paid subscription, but a free trial is available.
name: JetBrains Academy
title_url: https://www.jetbrains.com/academy/
-position: 6
+tags:
+ topics:
+ - general
+ - web development
+ - data science
+ payment_tiers:
+ - subscription
+ difficulty:
+ - beginner
+ type:
+ - interactive
diff --git a/pydis_site/apps/resources/resources/videos/jetbrains.yaml b/pydis_site/apps/resources/resources/jetbrains_videos.yaml
index 5d130db6..00d34e69 100644
--- a/pydis_site/apps/resources/resources/videos/jetbrains.yaml
+++ b/pydis_site/apps/resources/resources/jetbrains_videos.yaml
@@ -2,11 +2,20 @@ description: A collection of videos made by the PyCharm team at JetBrains on sub
Django, pytest and much more!<br><br>
Episodes of their "What does this package do?" series go over all sorts of libraries in Python
both in the standard library and from the community and give a video explanation of the key concepts.
+name: JetBrains YouTube Channel
icon_image: https://upload.wikimedia.org/wikipedia/commons/thumb/1/1a/JetBrains_Logo_2016.svg/1200px-JetBrains_Logo_2016.svg.png
icon_size: 50
title_image: https://resources.jetbrains.com/storage/products/pycharm/img/meta/pycharm_logo_300x300.png
-position: 3
-urls:
- - icon: branding/youtube
- url: https://www.youtube.com/channel/UCak6beUTLlVmf0E4AmnQkmw
- color: youtube-red
+title_url: https://www.youtube.com/channel/UCak6beUTLlVmf0E4AmnQkmw
+tags:
+ topics:
+ - general
+ - testing
+ - web development
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - video
diff --git a/pydis_site/apps/resources/resources/videos/jim_shaped_coding.yaml b/pydis_site/apps/resources/resources/jim_shaped_coding.yaml
index 488cfa83..c9727888 100644
--- a/pydis_site/apps/resources/resources/videos/jim_shaped_coding.yaml
+++ b/pydis_site/apps/resources/resources/jim_shaped_coding.yaml
@@ -5,9 +5,18 @@ description: 'JimShapedCoding contains a set of YouTube tutorials covering thing
<li><a href="https://www.youtube.com/watch?v=qMrAFscMBBc&list=PLOkVupluCIjvORWaF4kG-sXLgbVemYpEi">Django tutorials</a></li>
</ul>
Check out his channel for more videos!'
+name: JimShapedCoding
title_image: https://i.imgur.com/DlovZPf.png
-position: 5
-urls:
- - icon: branding/youtube
- url: https://www.youtube.com/channel/UCU8d7rcShA7MGuDyYH1aWGg
- color: youtube-red
+title_url: https://www.youtube.com/channel/UCU8d7rcShA7MGuDyYH1aWGg
+tags:
+ topics:
+ - general
+ - user interface
+ - web development
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - video
diff --git a/pydis_site/apps/resources/resources/kaggle_pandas_tutorial.yaml b/pydis_site/apps/resources/resources/kaggle_pandas_tutorial.yaml
new file mode 100644
index 00000000..c8e72c6e
--- /dev/null
+++ b/pydis_site/apps/resources/resources/kaggle_pandas_tutorial.yaml
@@ -0,0 +1,13 @@
+description: An interactive tutorial for learning Pandas, the most popular library for manipulating tabular data
+ in Python's data science ecosystem. This tutorial assumes some familiarity with writing code in notebooks.
+name: Kaggle Pandas Tutorial
+title_url: https://www.kaggle.com/learn/pandas
+tags:
+ topics:
+ - data science
+ payment_tiers:
+ - free
+ difficulty:
+ - intermediate
+ type:
+ - tutorial
diff --git a/pydis_site/apps/resources/resources/communities/kivy.yaml b/pydis_site/apps/resources/resources/kivy.yaml
index 601d7dba..b1f57483 100644
--- a/pydis_site/apps/resources/resources/communities/kivy.yaml
+++ b/pydis_site/apps/resources/resources/kivy.yaml
@@ -1,3 +1,4 @@
+name: Kivy
description: The Kivy project, through the Kivy framework and its sister projects,
aims to provide all the tools to create desktop and mobile applications in Python.
Allowing rapid development of multitouch applications with custom and exciting user interfaces.
@@ -5,14 +6,24 @@ icon_image: https://raw.githubusercontent.com/kivy/kivy-website/master/logos/kiv
icon_size: 50
title_image: https://i.imgur.com/EVP3jZR.png
title_url: https://discord.gg/djPtTRJ
-position: 5
urls:
+ - icon: solid/external-link-alt
+ url: https://kivy.org/
+ color: teal
- icon: branding/discord
url: https://discord.gg/djPtTRJ
color: blurple
- - icon: regular/link
- url: https://kivy.org/
- color: teal
- icon: branding/github
url: https://github.com/kivy
color: black
+tags:
+ topics:
+ - user interface
+ - game development
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - community
diff --git a/pydis_site/apps/resources/resources/communities/microsoft.yaml b/pydis_site/apps/resources/resources/microsoft.yaml
index b36c3a85..290283cc 100644
--- a/pydis_site/apps/resources/resources/communities/microsoft.yaml
+++ b/pydis_site/apps/resources/resources/microsoft.yaml
@@ -1,12 +1,20 @@
+name: Microsoft Python
description: Microsoft Python is a Discord server for discussing all things relating to using Python with Microsoft products,
they have channels for Azure, VS Code, IoT, Data Science and much more!
title_image: https://1000logos.net/wp-content/uploads/2017/04/Microsoft-Logo.png
-title_url: https://discord.gg/b8YJQPx
-position: 1
+title_url: https://www.microsoft.com/en-us/boards/pycon2020.aspx
urls:
- icon: branding/discord
url: https://discord.gg/b8YJQPx
color: blurple
- - icon: regular/link
- url: https://www.microsoft.com/en-us/boards/pycon2020.aspx
- color: teal
+tags:
+ topics:
+ - general
+ - tooling
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - community
diff --git a/pydis_site/apps/resources/resources/videos/microsoft.yaml b/pydis_site/apps/resources/resources/microsoft_videos.yaml
index 3ceaa1a2..f45aef63 100644
--- a/pydis_site/apps/resources/resources/videos/microsoft.yaml
+++ b/pydis_site/apps/resources/resources/microsoft_videos.yaml
@@ -7,12 +7,20 @@ description: A trove of tutorials & guides for developers from Microsoft's Devel
</ul>
Microsoft's Python Development Team also runs a Discord Server for discussions of Python in the Microsoft ecosystem,
including Visual Studio Code and Azure.
-title_image: https://img-prod-cms-rt-microsoft-com.akamaized.net/cms/api/am/imageFileData/RE2qVsJ?ver=3f74
-position: 4
+name: Microsoft Developer
+title_image: https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg
+title_url: https://www.youtube.com/channel/UCsMica-v34Irf9KVTh6xx-g
urls:
- - icon: branding/youtube
- url: https://www.youtube.com/channel/UCsMica-v34Irf9KVTh6xx-g
- color: youtube-red
- icon: branding/discord
url: https://aka.ms/python-discord
color: blurple
+tags:
+ topics:
+ - general
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ type:
+ - video
+ - community
diff --git a/pydis_site/apps/resources/resources/reading/books/mission_python.yaml b/pydis_site/apps/resources/resources/mission_python.yaml
index c4a48b7e..391a2983 100644
--- a/pydis_site/apps/resources/resources/reading/books/mission_python.yaml
+++ b/pydis_site/apps/resources/resources/mission_python.yaml
@@ -3,11 +3,18 @@ description: Learn programming and Python while building a complete and awesome
images, and walk-throughs make this a pleasure to both read and follow along. Excellent
book for beginners.
name: Mission Python
-position: 5
+title_url: https://www.sean.co.uk/books/mission-python/index.shtm
urls:
-- icon: regular/link
- url: https://www.sean.co.uk/books/mission-python/index.shtm
- color: teal
-- icon: branding/amazon
- url: https://www.amazon.com/Mission-Python-Code-Space-Adventure/dp/1593278578
- color: amazon-orange
+- icon: branding/goodreads
+ url: https://www.goodreads.com/book/show/35545850-mission-python
+ color: black
+tags:
+ topics:
+ - general
+ - game development
+ payment_tiers:
+ - paid
+ difficulty:
+ - beginner
+ type:
+ - book
diff --git a/pydis_site/apps/resources/resources/courses/mit_introduction_to_computer_science_and_programming.yaml b/pydis_site/apps/resources/resources/mit_introduction_to_computer_science_and_programming.yaml
index 5560b2cb..4e74936d 100644
--- a/pydis_site/apps/resources/resources/courses/mit_introduction_to_computer_science_and_programming.yaml
+++ b/pydis_site/apps/resources/resources/mit_introduction_to_computer_science_and_programming.yaml
@@ -3,4 +3,14 @@ description: This MITx offering teaches computer science with Python.
and the Python programming language itself.
name: 'MIT: Introduction to Computer Science and Programming'
title_url: https://www.edx.org/course/introduction-computer-science-mitx-6-00-1x-11
-position: 1
+tags:
+ topics:
+ - general
+ - algorithms and data structures
+ payment_tiers:
+ - free
+ - paid
+ difficulty:
+ - beginner
+ type:
+ - course
diff --git a/pydis_site/apps/resources/resources/tools/editors/mu_editor.yaml b/pydis_site/apps/resources/resources/mu_editor.yaml
index b92bac9d..b6318d0e 100644
--- a/pydis_site/apps/resources/resources/tools/editors/mu_editor.yaml
+++ b/pydis_site/apps/resources/resources/mu_editor.yaml
@@ -4,4 +4,12 @@ description: An editor aimed at beginners for the purpose of learning how to cod
with built-in tools to interact with Adafruit and Arduino boards.
name: Mu-Editor
title_url: https://codewith.mu/
-position: 3
+tags:
+ topics:
+ - microcontrollers
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ type:
+ - tool
diff --git a/pydis_site/apps/resources/resources/netbats_project_ideas.yaml b/pydis_site/apps/resources/resources/netbats_project_ideas.yaml
new file mode 100644
index 00000000..80ba771c
--- /dev/null
+++ b/pydis_site/apps/resources/resources/netbats_project_ideas.yaml
@@ -0,0 +1,14 @@
+description: A repository of project ideas to help one apply what they're learning, maintained by Python
+ community member Ned Batchelder, known on Python Discord as nedbat.
+name: Ned Batchelder's Kindling Projects
+title_url: https://nedbatchelder.com/text/kindling.html
+tags:
+ topics:
+ - general
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - project ideas
diff --git a/pydis_site/apps/resources/resources/reading/books/neural_networks_from_scratch_in_python.yaml b/pydis_site/apps/resources/resources/neural_networks_from_scratch_in_python.yaml
index 974b0e50..c4ad1e1b 100644
--- a/pydis_site/apps/resources/resources/reading/books/neural_networks_from_scratch_in_python.yaml
+++ b/pydis_site/apps/resources/resources/neural_networks_from_scratch_in_python.yaml
@@ -3,8 +3,17 @@ description: '"Neural Networks From Scratch" is a book intended to teach you how
This is so you can go out and do new/novel things with deep learning as well as to become more successful with even more basic models.
This book is to accompany the usual free tutorial videos and sample code from youtube.com/sentdex.'
name: Neural Networks from Scratch in Python
-position: 11
+title_url: https://nnfs.io/
urls:
- - icon: regular/link
- url: https://nnfs.io/
- color: teal
+ - icon: branding/goodreads
+ url: https://www.goodreads.com/book/show/55927899-neural-networks-from-scratch-in-python
+ color: black
+tags:
+ topics:
+ - data science
+ payment_tiers:
+ - paid
+ difficulty:
+ - intermediate
+ type:
+ - book
diff --git a/pydis_site/apps/resources/resources/communities/pallets.yaml b/pydis_site/apps/resources/resources/pallets.yaml
index 239b1491..a330b756 100644
--- a/pydis_site/apps/resources/resources/communities/pallets.yaml
+++ b/pydis_site/apps/resources/resources/pallets.yaml
@@ -1,13 +1,20 @@
+name: Pallets Projects
description: The Pallets Projects develop Python libraries such as the Flask web framework,
the Jinja templating library, and the Click command line toolkit. Join to discuss
and get help from the Pallets community.
title_image: https://i.imgur.com/sV9Ypdf.png
-title_url: https://discord.gg/t6rrQZH
-position: 6
+title_url: https://www.palletsprojects.com/
urls:
- icon: branding/discord
url: https://discord.gg/t6rrQZH
color: blurple
- - icon: regular/link
- url: https://www.palletsprojects.com/
- color: teal
+tags:
+ topics:
+ - web development
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - community
diff --git a/pydis_site/apps/resources/resources/communities/panda3d.yaml b/pydis_site/apps/resources/resources/panda3d.yaml
index 47797882..eeb54465 100644
--- a/pydis_site/apps/resources/resources/communities/panda3d.yaml
+++ b/pydis_site/apps/resources/resources/panda3d.yaml
@@ -1,12 +1,24 @@
+name: Panda3D
description: Panda3D is a Python-focused 3-D framework for rapid development of games,
visualizations, and simulations, written in C++ with an emphasis on performance and flexibility.
title_image: https://www.panda3d.org/wp-content/uploads/2019/01/panda3d_logo.png
title_url: https://discord.gg/9XsucTT
position: 9
urls:
+ - icon: solid/external-link-alt
+ url: https://www.panda3d.org/
+ color: teal
- icon: branding/discord
url: https://discord.gg/9XsucTT
color: blurple
- - icon: regular/link
- url: https://www.panda3d.org/
- color: teal
+tags:
+ topics:
+ - user interface
+ - game development
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - community
diff --git a/pydis_site/apps/resources/resources/communities/people_postgres_data.yaml b/pydis_site/apps/resources/resources/people_postgres_data.yaml
index 1c17d343..9fec6634 100644
--- a/pydis_site/apps/resources/resources/communities/people_postgres_data.yaml
+++ b/pydis_site/apps/resources/resources/people_postgres_data.yaml
@@ -1,3 +1,4 @@
+name: People, Postgres, Data
description: People, Postgres, Data specializes in building users of Postgres
and related ecosystem including but not limited to technologies such as RDS Postgres,
Aurora for Postgres, Google Postgres, PostgreSQL.Org Postgres, Greenplum, Timescale and ZomboDB.
@@ -5,14 +6,23 @@ description: People, Postgres, Data specializes in building users of Postgres
and Life in general including movies, games, books and travel.
title_image: https://media.discordapp.net/attachments/748954447857844318/750519488268730377/people_postgres_data.png
title_url: https://discord.gg/Ujw8m8v
-position: 2
urls:
+ - icon: solid/external-link-alt
+ url: https://postgresconf.org/
+ color: teal
- icon: branding/discord
url: https://discord.gg/Ujw8m8v
color: bluple
- - icon: regular/link
- url: https://postgresconf.org/
- color: teal
- icon: branding/reddit
url: https://reddit.com/r/postgresql
color: orangered
+tags:
+ topics:
+ - databases
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - community
diff --git a/pydis_site/apps/resources/resources/podcasts/podcast_dunder_init.yaml b/pydis_site/apps/resources/resources/podcast_dunder_init.yaml
index efe1601f..2751481a 100644
--- a/pydis_site/apps/resources/resources/podcasts/podcast_dunder_init.yaml
+++ b/pydis_site/apps/resources/resources/podcast_dunder_init.yaml
@@ -2,4 +2,13 @@ description: The podcast about Python and the people who make it great. Weekly l
interviews with the creators of notable Python packages.
name: Podcast.__init__
title_url: https://www.podcastinit.com/
-position: 2
+tags:
+ topics:
+ - general
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - podcast
diff --git a/pydis_site/apps/resources/resources/podcasts/_category_info.yaml b/pydis_site/apps/resources/resources/podcasts/_category_info.yaml
deleted file mode 100644
index 1d2d3ba5..00000000
--- a/pydis_site/apps/resources/resources/podcasts/_category_info.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-description: Notable podcasts about the Python ecosystem.
-name: Podcasts
-default_icon: regular/microphone-alt
-default_icon_color: black
diff --git a/pydis_site/apps/resources/resources/courses/practical_python_programming.yaml b/pydis_site/apps/resources/resources/practical_python_programming.yaml
index b801ca8c..12873b7c 100644
--- a/pydis_site/apps/resources/resources/courses/practical_python_programming.yaml
+++ b/pydis_site/apps/resources/resources/practical_python_programming.yaml
@@ -7,3 +7,12 @@ description: Created and taught by <a href="https://dabeaz.com/">David Beazley</
name: Practical Python Programming
title_url: https://dabeaz-course.github.io/practical-python/
position: 4
+tags:
+ topics:
+ - general
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ type:
+ - course
diff --git a/pydis_site/apps/resources/resources/tools/ides/pycharm.yaml b/pydis_site/apps/resources/resources/pycharm.yaml
index b959b0f8..574158bc 100644
--- a/pydis_site/apps/resources/resources/tools/ides/pycharm.yaml
+++ b/pydis_site/apps/resources/resources/pycharm.yaml
@@ -2,4 +2,13 @@ description: The very best Python IDE, with a wealth of advanced features and co
functions.
name: PyCharm
title_url: https://www.jetbrains.com/pycharm/
-position: 0
+tags:
+ topics:
+ - general
+ payment_tiers:
+ - free
+ - paid
+ difficulty:
+ - intermediate
+ type:
+ - tool
diff --git a/pydis_site/apps/resources/resources/communities/pyglet.yaml b/pydis_site/apps/resources/resources/pyglet.yaml
index 784f514e..bdfb84cf 100644
--- a/pydis_site/apps/resources/resources/communities/pyglet.yaml
+++ b/pydis_site/apps/resources/resources/pyglet.yaml
@@ -1,15 +1,23 @@
+name: Pyglet
description: Pyglet is a powerful,
yet easy to use Python library for developing games and other visually-rich applications on Windows,
Mac OS X and Linux. It supports windowing, user interface event handling, Joysticks, OpenGL graphics,
loading images and videos, and playing sounds and music. All of this with a friendly Pythonic API,
that's simple to learn and doesn't get in your way.
title_image: https://i.imgur.com/LfQwXUe.png
-title_url: https://discord.gg/QXyegWe
-position: 8
+title_url: http://pyglet.org/
urls:
- icon: branding/discord
url: https://discord.gg/QXyegWe
color: blurple
- - icon: regular/link
- url: http://pyglet.org/
- color: teal
+tags:
+ topics:
+ - user interface
+ - game development
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - community
diff --git a/pydis_site/apps/resources/resources/podcasts/python_bytes.yaml b/pydis_site/apps/resources/resources/python_bytes.yaml
index 4f817f26..9beba4f4 100644
--- a/pydis_site/apps/resources/resources/podcasts/python_bytes.yaml
+++ b/pydis_site/apps/resources/resources/python_bytes.yaml
@@ -3,3 +3,13 @@ description: A byte-sized podcast where Michael Kennedy and Brian Okken work thr
name: Python Bytes
title_url: https://pythonbytes.fm/
position: 1
+tags:
+ topics:
+ - general
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - podcast
diff --git a/pydis_site/apps/resources/resources/reading/tutorials/python_cheat_sheet.yaml b/pydis_site/apps/resources/resources/python_cheat_sheet.yaml
index 70ac49ef..56f61165 100644
--- a/pydis_site/apps/resources/resources/reading/tutorials/python_cheat_sheet.yaml
+++ b/pydis_site/apps/resources/resources/python_cheat_sheet.yaml
@@ -2,4 +2,12 @@ description: A Python 3 cheat sheet with useful information and tips, as well as
pitfalls for beginners. This is a PDF.
name: Python Cheat Sheet
title_url: https://perso.limsi.fr/pointal/_media/python:cours:mementopython3-english.pdf
-position: 6
+tags:
+ topics:
+ - general
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ type:
+ - tutorial
diff --git a/pydis_site/apps/resources/resources/python_cookbook.yaml b/pydis_site/apps/resources/resources/python_cookbook.yaml
new file mode 100644
index 00000000..bc05d743
--- /dev/null
+++ b/pydis_site/apps/resources/resources/python_cookbook.yaml
@@ -0,0 +1,21 @@
+description: A book full of very smart problem-solving recipes for various Python topics,
+ including moving from Python 2 to Python 3.
+name: Python Cookbook
+title_url: http://shop.oreilly.com/product/0636920027072.do
+urls:
+- icon: branding/goodreads
+ url: https://www.goodreads.com/book/show/17152735-python-cookbook
+ color: black
+- icon: branding/github
+ url: https://github.com/dabeaz/python-cookbook
+ color: black
+tags:
+ topics:
+ - general
+ - software design
+ payment_tiers:
+ - paid
+ difficulty:
+ - intermediate
+ type:
+ - book
diff --git a/pydis_site/apps/resources/resources/reading/books/python_crash_course.yaml b/pydis_site/apps/resources/resources/python_crash_course.yaml
index 3cbf19c8..d916075e 100644
--- a/pydis_site/apps/resources/resources/reading/books/python_crash_course.yaml
+++ b/pydis_site/apps/resources/resources/python_crash_course.yaml
@@ -7,14 +7,21 @@ description: "This fast-paced, thorough introduction to programming with Python
a Space Invaders–inspired arcade game, a set of data visualizations with Python’s handy libraries,
and a simple web app you can deploy online."
name: Python Crash Course
-position: 12
+title_url: https://nostarch.com/pythoncrashcourse2e
urls:
- - icon: regular/link
- url: https://nostarch.com/pythoncrashcourse2e
- color: teal
- - icon: branding/amazon
- url: https://www.amazon.com/Python-Crash-Course-Project-Based-Introduction/dp/1593276036
- color: amazon-orange
+ - icon: branding/goodreads
+ url: https://www.goodreads.com/book/show/23241059-python-crash-course
+ color: black
- icon: branding/github
url: https://ehmatthes.github.io/pcc/
color: black
+tags:
+ topics:
+ - general
+ - game development
+ payment_tiers:
+ - paid
+ difficulty:
+ - beginner
+ type:
+ - book
diff --git a/pydis_site/apps/resources/resources/reading/tutorials/python_developer_guide.yaml b/pydis_site/apps/resources/resources/python_developer_guide.yaml
index 625d57c8..2806d75d 100644
--- a/pydis_site/apps/resources/resources/reading/tutorials/python_developer_guide.yaml
+++ b/pydis_site/apps/resources/resources/python_developer_guide.yaml
@@ -2,4 +2,12 @@ description: This guide is a comprehensive resource for contributing to Python �
It is maintained by the same community that maintains Python.
name: Python Developer's Guide
title_url: https://devguide.python.org/
-position: 2
+tags:
+ topics:
+ - general
+ payment_tiers:
+ - free
+ difficulty:
+ - intermediate
+ type:
+ - tutorial
diff --git a/pydis_site/apps/resources/resources/python_discord_videos.yaml b/pydis_site/apps/resources/resources/python_discord_videos.yaml
new file mode 100644
index 00000000..012ec8ea
--- /dev/null
+++ b/pydis_site/apps/resources/resources/python_discord_videos.yaml
@@ -0,0 +1,16 @@
+name: Python Discord YouTube Channel
+description: It's our YouTube channel! We are slowly gathering content here directly related to Python,
+ our community and the events we host. Come check us out!
+title_image: https://raw.githubusercontent.com/python-discord/branding/master/logos/logo_banner/logo_site_banner_dark_512.png
+title_url: https://www.youtube.com/pythondiscord
+tags:
+ topics:
+ - general
+ - software design
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - video
diff --git a/pydis_site/apps/resources/resources/interactive/python_morsels.yaml b/pydis_site/apps/resources/resources/python_morsels.yaml
index 879500eb..bbc8133b 100644
--- a/pydis_site/apps/resources/resources/interactive/python_morsels.yaml
+++ b/pydis_site/apps/resources/resources/python_morsels.yaml
@@ -7,4 +7,13 @@ description: 'Learn to write more idiomatic Python code with deliberate practice
tests and some may include bonuses for a little more of a challenge!'
name: Python Morsels
title_url: https://www.pythonmorsels.com/
-position: 3
+tags:
+ topics:
+ - general
+ - software design
+ payment_tiers:
+ - subscription
+ difficulty:
+ - intermediate
+ type:
+ - interactive
diff --git a/pydis_site/apps/resources/resources/communities/subreddit.yaml b/pydis_site/apps/resources/resources/python_subreddit.yaml
index d3ddb15a..e94f84fc 100644
--- a/pydis_site/apps/resources/resources/communities/subreddit.yaml
+++ b/pydis_site/apps/resources/resources/python_subreddit.yaml
@@ -4,3 +4,13 @@ title_icon: branding/reddit
title_icon_color: orangered
title_url: https://www.reddit.com/r/Python/
position: 0
+tags:
+ topics:
+ - general
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - community
diff --git a/pydis_site/apps/resources/resources/python_tricks.yaml b/pydis_site/apps/resources/resources/python_tricks.yaml
new file mode 100644
index 00000000..aa1b2fcd
--- /dev/null
+++ b/pydis_site/apps/resources/resources/python_tricks.yaml
@@ -0,0 +1,19 @@
+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
+title_url: https://realpython.com/products/python-tricks-book/
+urls:
+- icon: branding/goodreads
+ url: https://www.goodreads.com/book/show/36990732-python-tricks
+ color: black
+tags:
+ topics:
+ - general
+ - software design
+ payment_tiers:
+ - paid
+ difficulty:
+ - intermediate
+ type:
+ - book
diff --git a/pydis_site/apps/resources/resources/interactive/python_tutor.yaml b/pydis_site/apps/resources/resources/python_tutor.yaml
index 64b50d09..6bee0d69 100644
--- a/pydis_site/apps/resources/resources/interactive/python_tutor.yaml
+++ b/pydis_site/apps/resources/resources/python_tutor.yaml
@@ -1,4 +1,14 @@
description: Write Python code in your web browser, and see it visualized step by step.
name: Python Tutor
title_url: https://www.pythontutor.com/
-position: 2
+tags:
+ topics:
+ - general
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - tool
+ - interactive
diff --git a/pydis_site/apps/resources/resources/reading/_category_info.yaml b/pydis_site/apps/resources/resources/reading/_category_info.yaml
deleted file mode 100644
index 64b87e47..00000000
--- a/pydis_site/apps/resources/resources/reading/_category_info.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
-description: Books and tutorials related to Python and popular third-party libraries and frameworks.
-name: Reading
diff --git a/pydis_site/apps/resources/resources/reading/books/_category_info.yaml b/pydis_site/apps/resources/resources/reading/books/_category_info.yaml
deleted file mode 100644
index ae092a20..00000000
--- a/pydis_site/apps/resources/resources/reading/books/_category_info.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-description: The best books for learning Python or Python Frameworks.
-name: Books
-default_icon: branding/python
-default_icon_color: black
-position: 0
diff --git a/pydis_site/apps/resources/resources/reading/books/effective_python.yaml b/pydis_site/apps/resources/resources/reading/books/effective_python.yaml
deleted file mode 100644
index becd0578..00000000
--- a/pydis_site/apps/resources/resources/reading/books/effective_python.yaml
+++ /dev/null
@@ -1,15 +0,0 @@
-description: A book that gives 90 best practices for writing excellent Python. Great
- for intermediates.
-name: Effective Python
-position: 3
-urls:
-- icon: regular/link
- url: https://effectivepython.com/
- color: teal
-- icon: branding/amazon
- url: https://www.amazon.com/Effective-Python-Specific-Software-Development/dp/0134853989
- color: amazon-orange
- title: Amazon
-- icon: branding/github
- url: https://github.com/bslatkin/effectivepython
- color: black
diff --git a/pydis_site/apps/resources/resources/reading/books/flask_web_development.yaml b/pydis_site/apps/resources/resources/reading/books/flask_web_development.yaml
deleted file mode 100644
index d191f02d..00000000
--- a/pydis_site/apps/resources/resources/reading/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
-position: 6
-urls:
-- icon: regular/link
- url: https://shop.oreilly.com/product/0636920031116.do
- color: teal
-- icon: branding/amazon
- url: https://www.amazon.com/Flask-Web-Development-Developing-Applications/dp/1449372627
- color: amazon-orange
-- icon: branding/github
- url: https://github.com/miguelgrinberg/flasky
- color: black
diff --git a/pydis_site/apps/resources/resources/reading/books/fluent_python.yaml b/pydis_site/apps/resources/resources/reading/books/fluent_python.yaml
deleted file mode 100644
index 92f4bbab..00000000
--- a/pydis_site/apps/resources/resources/reading/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. By far the most recommended book for intermediates.
-name: Fluent Python
-position: 7
-urls:
-- icon: regular/link
- url: https://www.oreilly.com/library/view/fluent-python/9781491946237/
- color: teal
-- icon: branding/amazon
- url: https://www.amazon.com/Fluent-Python-Concise-Effective-Programming/dp/1491946008
- color: amazon-orange
-- icon: branding/github
- url: https://github.com/fluentpython
- color: black
diff --git a/pydis_site/apps/resources/resources/reading/books/hitchhikers_guide_to_python.yaml b/pydis_site/apps/resources/resources/reading/books/hitchhikers_guide_to_python.yaml
deleted file mode 100644
index 906860c7..00000000
--- a/pydis_site/apps/resources/resources/reading/books/hitchhikers_guide_to_python.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-description: A best practice handbook for both novice and expert Python developers to the installation,
- configuration, and usage of Python on a daily basis.
-name: The Hitchhiker's Guide to Python
-position: 0
-urls:
-- icon: regular/link
- url: https://python-guide.org/
- color: teal
-- icon: branding/amazon
- url: https://www.amazon.com/Hitchhikers-Guide-Python-Practices-Development/dp/1491933178/
- color: amazon-orange
diff --git a/pydis_site/apps/resources/resources/reading/books/python_cookbook.yaml b/pydis_site/apps/resources/resources/reading/books/python_cookbook.yaml
deleted file mode 100644
index c939ab9e..00000000
--- a/pydis_site/apps/resources/resources/reading/books/python_cookbook.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-description: A book full of very smart problem-solving recipes for various Python topics,
- including moving from Python 2 to Python 3.
-name: Python Cookbook
-position: 8
-urls:
-- icon: regular/link
- url: https://shop.oreilly.com/product/0636920027072.do
- color: teal
-- icon: branding/amazon
- url: https://www.amazon.com/Python-Cookbook-Third-David-Beazley/dp/1449340377
- color: amazon-orange
-- icon: branding/github
- url: https://github.com/dabeaz/python-cookbook
- color: black
diff --git a/pydis_site/apps/resources/resources/reading/books/python_tricks.yaml b/pydis_site/apps/resources/resources/reading/books/python_tricks.yaml
deleted file mode 100644
index c0941809..00000000
--- a/pydis_site/apps/resources/resources/reading/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
-position: 4
-urls:
-- icon: regular/link
- url: https://realpython.com/products/python-tricks-book/
- color: teal
-- icon: branding/amazon
- url: https://www.amazon.com/Python-Tricks-Buffet-Awesome-Features/dp/1775093301
- color: amazon-orange
diff --git a/pydis_site/apps/resources/resources/reading/books/two_scoops_of_django.yaml b/pydis_site/apps/resources/resources/reading/books/two_scoops_of_django.yaml
deleted file mode 100644
index 7d83e7c4..00000000
--- a/pydis_site/apps/resources/resources/reading/books/two_scoops_of_django.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-description: Tips, tricks, and best practices for your Django project.
- A highly recommended resource for Django web developers.
-name: Two Scoops of Django
-position: 9
-urls:
-- icon: regular/link
- url: https://twoscoopspress.com/products/two-scoops-of-django-1-11
- color: teal
-- icon: branding/amazon
- url: https://www.amazon.com/Two-Scoops-Django-Best-Practices/dp/0981467342
- color: amazon-orange
-- icon: branding/github
- url: https://github.com/twoscoops/two-scoops-of-django-2.0-code-examples
- color: black
diff --git a/pydis_site/apps/resources/resources/reading/tutorials/_category_info.yaml b/pydis_site/apps/resources/resources/reading/tutorials/_category_info.yaml
deleted file mode 100644
index a18b837d..00000000
--- a/pydis_site/apps/resources/resources/reading/tutorials/_category_info.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-description: Tutorials and references for those that are just getting started with Python.
-name: Tutorials
-default_icon: branding/python
-default_icon_color: black
-position: 1
diff --git a/pydis_site/apps/resources/resources/communities/real_python.yaml b/pydis_site/apps/resources/resources/real_python.yaml
index 1fc74d93..93953004 100644
--- a/pydis_site/apps/resources/resources/communities/real_python.yaml
+++ b/pydis_site/apps/resources/resources/real_python.yaml
@@ -1,12 +1,22 @@
+name: Real Python
description: Dan Bader's treasure trove of quizzes, tutorials and interactive content for learning Python.
An absolute goldmine.
title_image: https://i.imgur.com/WDqhZ36.png
title_url: https://realpython.com/
position: 3
urls:
- - icon: regular/link
- url: https://realpython.com/
- color: teal
- icon: branding/youtube
url: https://www.youtube.com/channel/UCI0vQvr9aFn27yR6Ej6n5UA
color: youtube-red
+tags:
+ topics:
+ - general
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - tutorial
+ - video
+ - community
diff --git a/pydis_site/apps/resources/resources/regex101.yaml b/pydis_site/apps/resources/resources/regex101.yaml
new file mode 100644
index 00000000..45d00f1b
--- /dev/null
+++ b/pydis_site/apps/resources/resources/regex101.yaml
@@ -0,0 +1,15 @@
+description: An online tool for testing regular expressions that helps you understand what the regular expression can
+ match. Remember to set the "flavor" to Python.
+name: regex101
+title_url: https://regex101.com/
+tags:
+ topics:
+ - general
+ - other
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - tool
diff --git a/pydis_site/apps/resources/resources/repl_it.yaml b/pydis_site/apps/resources/resources/repl_it.yaml
new file mode 100644
index 00000000..e0f6cbb3
--- /dev/null
+++ b/pydis_site/apps/resources/resources/repl_it.yaml
@@ -0,0 +1,14 @@
+description: A free, collaborative, in-browser IDE to code in 50+ languages —
+ without spending a second on setup.
+name: repl.it
+title_url: https://repl.it/
+tags:
+ topics:
+ - general
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - tool
diff --git a/pydis_site/apps/resources/resources/tools/accessibility_tools/screen_readers.yaml b/pydis_site/apps/resources/resources/screen_readers.yaml
index 39372956..b086b301 100644
--- a/pydis_site/apps/resources/resources/tools/accessibility_tools/screen_readers.yaml
+++ b/pydis_site/apps/resources/resources/screen_readers.yaml
@@ -4,4 +4,14 @@ description: Screen readers are software programs that allow blind
with this link describing many of them and their capabilities.
name: Screen Readers - American Foundation for the Blind
title_url: https://www.afb.org/blindness-and-low-vision/using-technology/assistive-technology-products/screen-readers
-position: 1
+tags:
+ topics:
+ - other
+ payment_tiers:
+ - free
+ - paid
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - tool
diff --git a/pydis_site/apps/resources/resources/videos/sentdex.yaml b/pydis_site/apps/resources/resources/sentdex.yaml
index 4e5f54c6..7cb0a8a4 100644
--- a/pydis_site/apps/resources/resources/videos/sentdex.yaml
+++ b/pydis_site/apps/resources/resources/sentdex.yaml
@@ -1,3 +1,4 @@
+name: Sentdex
description: 'An enormous amount of Python content for all skill levels
from the most popular Python YouTuber on the web.
<ul>
@@ -9,14 +10,24 @@ description: 'An enormous amount of Python content for all skill levels
Check out his channel for more video series!
'
title_image: https://i.imgur.com/kJgWZIu.png
-position: 1
+title_url: https://www.youtube.com/user/sentdex
urls:
- - icon: branding/youtube
- url: https://www.youtube.com/user/sentdex
- color: youtube-red
+ - icon: solid/external-link-alt
+ url: https://pythonprogramming.net/
+ color: teal
- icon: branding/discord
url: https://discord.gg/sentdex
color: blurple
- - icon: regular/link
- url: https://pythonprogramming.net/
- color: teal
+tags:
+ topics:
+ - general
+ - user interface
+ - data science
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - video
+ - community
diff --git a/pydis_site/apps/resources/resources/reading/tutorials/simple_guide_to_git.yaml b/pydis_site/apps/resources/resources/simple_guide_to_git.yaml
index 9d151bf9..3bb46e6d 100644
--- a/pydis_site/apps/resources/resources/reading/tutorials/simple_guide_to_git.yaml
+++ b/pydis_site/apps/resources/resources/simple_guide_to_git.yaml
@@ -3,4 +3,12 @@ name: A Simple Guide to Git
title_url: https://rogerdudler.github.io/git-guide/
title_icon: branding/github
title_icon_color: black
-position: 4
+tags:
+ topics:
+ - tooling
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ type:
+ - tutorial
diff --git a/pydis_site/apps/resources/resources/socratica.yaml b/pydis_site/apps/resources/resources/socratica.yaml
new file mode 100644
index 00000000..45150b33
--- /dev/null
+++ b/pydis_site/apps/resources/resources/socratica.yaml
@@ -0,0 +1,23 @@
+name: Socratica
+description: 'Socratica is a small studio focused on producing high quality STEM-related educational content,
+including a series about Python. Their videos star actress Ulka Simone Mohanty, who plays an android-like
+instructor explaining fundamental concepts in a concise and entertaining way.'
+title_image: https://i.imgur.com/4SoHeLz.png
+title_url: https://www.youtube.com/playlist?list=PLi01XoE8jYohWFPpC17Z-wWhPOSuh8Er-
+urls:
+ - icon: solid/database
+ url: https://www.youtube.com/playlist?list=PLi01XoE8jYojRqM4qGBF1U90Ee1Ecb5tt
+ color: teal
+ - icon: branding/youtube
+ url: https://www.youtube.com/channel/UCW6TXMZ5Pq6yL6_k5NZ2e0Q
+ color: youtube-red
+tags:
+ topics:
+ - general
+ - databases
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ type:
+ - video
diff --git a/pydis_site/apps/resources/resources/interactive/sololearn.yaml b/pydis_site/apps/resources/resources/sololearn.yaml
index 51dceb2a..998f5368 100644
--- a/pydis_site/apps/resources/resources/interactive/sololearn.yaml
+++ b/pydis_site/apps/resources/resources/sololearn.yaml
@@ -4,4 +4,14 @@ description: SoloLearn's Python 3 course serves as a simple and convenient intro
and mobile apps being available to use.
name: SoloLearn
title_url: https://www.sololearn.com/Course/Python/
-position: 4
+tags:
+ topics:
+ - general
+ payment_tiers:
+ - free
+ - subscription
+ difficulty:
+ - beginner
+ type:
+ - interactive
+ - course
diff --git a/pydis_site/apps/resources/resources/tools/ides/spyder.yaml b/pydis_site/apps/resources/resources/spyder.yaml
index c2f9c2dc..668e9306 100644
--- a/pydis_site/apps/resources/resources/tools/ides/spyder.yaml
+++ b/pydis_site/apps/resources/resources/spyder.yaml
@@ -2,4 +2,13 @@ description: The Scientific Python Development Environment.
Simpler and lighter than PyCharm, but still packs a punch.
name: Spyder
title_url: https://www.spyder-ide.org/
-position: 1
+tags:
+ topics:
+ - data science
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - tool
diff --git a/pydis_site/apps/resources/resources/tools/editors/sublime_text.yaml b/pydis_site/apps/resources/resources/sublime_text.yaml
index 3c6e7e84..05596477 100644
--- a/pydis_site/apps/resources/resources/tools/editors/sublime_text.yaml
+++ b/pydis_site/apps/resources/resources/sublime_text.yaml
@@ -2,4 +2,13 @@ description: A powerful Python-backed editor with great community support and a
of extensions.
name: Sublime Text
title_url: https://www.sublimetext.com/
-position: 2
+tags:
+ topics:
+ - general
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - tool
diff --git a/pydis_site/apps/resources/resources/podcasts/talk_python_to_me.yaml b/pydis_site/apps/resources/resources/talk_python_to_me.yaml
index 5ce21fd7..509922c3 100644
--- a/pydis_site/apps/resources/resources/podcasts/talk_python_to_me.yaml
+++ b/pydis_site/apps/resources/resources/talk_python_to_me.yaml
@@ -2,4 +2,13 @@ description: The essential weekly Python podcast. Michael Kennedy and a prominen
name within the Python community dive into a topic that relates to their experience.
name: Talk Python To Me
title_url: https://talkpython.fm/
-position: 0
+tags:
+ topics:
+ - general
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - podcast
diff --git a/pydis_site/apps/resources/resources/tools/accessibility_tools/talon_voice.yaml b/pydis_site/apps/resources/resources/talon_voice.yaml
index 9df5f66f..3be5fe20 100644
--- a/pydis_site/apps/resources/resources/tools/accessibility_tools/talon_voice.yaml
+++ b/pydis_site/apps/resources/resources/talon_voice.yaml
@@ -3,4 +3,13 @@ description: Talon is a tool being built that aims to bring programming,
who have limited or no use of their hands.
name: Talon Voice
title_url: https://talonvoice.com/
-position: 0
+tags:
+ topics:
+ - other
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - tool
diff --git a/pydis_site/apps/resources/resources/podcasts/test_and_code.yaml b/pydis_site/apps/resources/resources/test_and_code.yaml
index d5751577..f0d1c3b3 100644
--- a/pydis_site/apps/resources/resources/podcasts/test_and_code.yaml
+++ b/pydis_site/apps/resources/resources/test_and_code.yaml
@@ -2,4 +2,14 @@ description: Brian Okken's weekly podcast on testing. Usually deals with Python,
but also covers many language-agnostic topics from the testing and DevOps world.
name: Test & Code
title_url: https://testandcode.com/
-position: 3
+tags:
+ topics:
+ - testing
+ - tooling
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - podcast
diff --git a/pydis_site/apps/resources/resources/reading/tutorials/the_flask_mega_tutorial.yaml b/pydis_site/apps/resources/resources/the_flask_mega_tutorial.yaml
index 8d61ea73..151768a5 100644
--- a/pydis_site/apps/resources/resources/reading/tutorials/the_flask_mega_tutorial.yaml
+++ b/pydis_site/apps/resources/resources/the_flask_mega_tutorial.yaml
@@ -1,4 +1,13 @@
description: Miguel Grinberg's fully featured mega-tutorial for learning how to create web applications with the Flask framework.
name: The Flask Mega-Tutorial
title_url: https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world
-position: 5
+tags:
+ topics:
+ - web development
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - tutorial
diff --git a/pydis_site/apps/resources/resources/podcasts/the_real_python_podcast.yaml b/pydis_site/apps/resources/resources/the_real_python_podcast.yaml
index dea894ea..647779d5 100644
--- a/pydis_site/apps/resources/resources/podcasts/the_real_python_podcast.yaml
+++ b/pydis_site/apps/resources/resources/the_real_python_podcast.yaml
@@ -4,4 +4,13 @@ description: A weekly Python podcast hosted by Christopher Bailey with interview
career tips, and related software development topics.
name: The Real Python Podcast
title_url: https://realpython.com/podcasts/rpp/
-position: 4
+tags:
+ topics:
+ - general
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - podcast
diff --git a/pydis_site/apps/resources/resources/reading/books/think_python.yaml b/pydis_site/apps/resources/resources/think_python.yaml
index 6de87043..7099afd8 100644
--- a/pydis_site/apps/resources/resources/reading/books/think_python.yaml
+++ b/pydis_site/apps/resources/resources/think_python.yaml
@@ -4,14 +4,21 @@ description: Think Python is an introduction to Python programming for beginners
Larger pieces, like recursion and object-oriented programming are divided into a sequence of smaller steps
and introduced over the course of several chapters.
name: Think Python
-position: 10
+title_url: https://greenteapress.com/wp/think-python-2e/
urls:
- - icon: regular/link
- url: https://greenteapress.com/wp/think-python-2e/
- color: teal
- - icon: branding/amazon
- url: https://www.amazon.com/gp/product/1491939362
- color: amazon-orange
+ - icon: branding/goodreads
+ url: https://www.goodreads.com/book/show/14514306-think-python
+ color: black
- icon: branding/github
url: https://github.com/AllenDowney/ThinkPython2
color: black
+tags:
+ topics:
+ - general
+ - software design
+ payment_tiers:
+ - paid
+ difficulty:
+ - beginner
+ type:
+ - book
diff --git a/pydis_site/apps/resources/resources/tools/ides/thonny.yaml b/pydis_site/apps/resources/resources/thonny.yaml
index d7f03a74..29ba9e07 100644
--- a/pydis_site/apps/resources/resources/tools/ides/thonny.yaml
+++ b/pydis_site/apps/resources/resources/thonny.yaml
@@ -3,3 +3,12 @@ description: A Python IDE specifically aimed at learning programming. Has a lot
name: Thonny
title_url: https://thonny.org/
position: 2
+tags:
+ topics:
+ - general
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ type:
+ - tool
diff --git a/pydis_site/apps/resources/resources/tools/_category_info.yaml b/pydis_site/apps/resources/resources/tools/_category_info.yaml
deleted file mode 100644
index 6b16baa6..00000000
--- a/pydis_site/apps/resources/resources/tools/_category_info.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-description: This page is a curated list of tools that we regularly recommend in the community.
- If you have a suggestion for something to add to this page, please create an issue in
- <a href="https://github.com/python-discord/meta/issues">our meta repo</a>, and we'll consider adding it.
-name: Tools
diff --git a/pydis_site/apps/resources/resources/tools/accessibility_tools/_category_info.yaml b/pydis_site/apps/resources/resources/tools/accessibility_tools/_category_info.yaml
deleted file mode 100644
index e770db07..00000000
--- a/pydis_site/apps/resources/resources/tools/accessibility_tools/_category_info.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-description: Accessibility tools that help people write Python code.
-name: Accessibility Tools
-default_icon: branding/python
-default_icon_color: black
-position: 2
diff --git a/pydis_site/apps/resources/resources/tools/editors/_category_info.yaml b/pydis_site/apps/resources/resources/tools/editors/_category_info.yaml
deleted file mode 100644
index 3cdfff3a..00000000
--- a/pydis_site/apps/resources/resources/tools/editors/_category_info.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-description: Lightweight code editors supporting Python
-name: Editors
-default_icon: branding/python
-default_icon_color: black
-position: 1
diff --git a/pydis_site/apps/resources/resources/tools/editors/google_collab.yaml b/pydis_site/apps/resources/resources/tools/editors/google_collab.yaml
deleted file mode 100644
index 302c3e2e..00000000
--- a/pydis_site/apps/resources/resources/tools/editors/google_collab.yaml
+++ /dev/null
@@ -1,7 +0,0 @@
-description: Google Collab is a high-powered custom version of Jupyter Notebook which supports e.g.
- !apt-get to install arbitrary Debian packages to the runtime, which is very generous with CPU and memory,
- and well-integrated with Google Drive.
- You can share your Collab Notebooks with other people and work collaboratively.
-name: Google Collab
-title_url: https://colab.research.google.com/notebooks/intro.ipynb
-position: 4
diff --git a/pydis_site/apps/resources/resources/tools/ides/_category_info.yaml b/pydis_site/apps/resources/resources/tools/ides/_category_info.yaml
deleted file mode 100644
index 614625a6..00000000
--- a/pydis_site/apps/resources/resources/tools/ides/_category_info.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-description: Fully-integrated development environments for serious Python work.
-name: IDEs
-default_icon: branding/python
-default_icon_color: black
-position: 0
diff --git a/pydis_site/apps/resources/resources/tools/ides/replit.yaml b/pydis_site/apps/resources/resources/tools/ides/replit.yaml
deleted file mode 100644
index 844c5016..00000000
--- a/pydis_site/apps/resources/resources/tools/ides/replit.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-description: A free, collaborative, in-browser IDE to code in 50+ languages —
- without spending a second on setup.
-name: replit
-title_url: https://replit.com/
-position: 3
diff --git a/pydis_site/apps/resources/resources/two_scoops_of_django.yaml b/pydis_site/apps/resources/resources/two_scoops_of_django.yaml
new file mode 100644
index 00000000..f372d35d
--- /dev/null
+++ b/pydis_site/apps/resources/resources/two_scoops_of_django.yaml
@@ -0,0 +1,20 @@
+description: Tips, tricks, and best practices for your Django project.
+ A highly recommended resource for Django web developers.
+name: Two Scoops of Django
+title_url: https://www.feldroy.com/books/two-scoops-of-django-3-x
+urls:
+- icon: branding/goodreads
+ url: https://www.goodreads.com/book/show/55822151-two-scoops-of-django-3-x
+ color: black
+- icon: branding/github
+ url: https://github.com/twoscoops/two-scoops-of-django-2.0-code-examples
+ color: black
+tags:
+ topics:
+ - web development
+ payment_tiers:
+ - paid
+ difficulty:
+ - intermediate
+ type:
+ - book
diff --git a/pydis_site/apps/resources/resources/courses/university_of_michigan.yaml b/pydis_site/apps/resources/resources/university_of_michigan.yaml
index 3efe7640..7aaaf2ae 100644
--- a/pydis_site/apps/resources/resources/courses/university_of_michigan.yaml
+++ b/pydis_site/apps/resources/resources/university_of_michigan.yaml
@@ -2,4 +2,12 @@ description: A 5-part specialization course that teaches Python from scratch.
The course has no pre-requisites and avoids all but the simplest mathematics.
name: 'University of Michigan: Programming for Everybody'
title_url: https://www.coursera.org/learn/python
-position: 2
+tags:
+ topics:
+ - general
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ type:
+ - course
diff --git a/pydis_site/apps/resources/resources/courses/university_of_toronto.yaml b/pydis_site/apps/resources/resources/university_of_toronto.yaml
index 0a7839de..94df96f2 100644
--- a/pydis_site/apps/resources/resources/courses/university_of_toronto.yaml
+++ b/pydis_site/apps/resources/resources/university_of_toronto.yaml
@@ -1,7 +1,6 @@
description: A 2-part course that teaches Python. Primarily intended for high school students
and first-year university students who want to learn programming.
name: 'University of Toronto: Learn to Program'
-position: 0
urls:
- icon: regular/graduation-cap
url: https://www.coursera.org/learn/learn-to-program
@@ -9,3 +8,13 @@ urls:
- icon: regular/graduation-cap
url: https://www.coursera.org/learn/program-code
color: youtube-red
+tags:
+ topics:
+ - general
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - course
diff --git a/pydis_site/apps/resources/resources/vcokltfre_discord_bot_tutorial.yaml b/pydis_site/apps/resources/resources/vcokltfre_discord_bot_tutorial.yaml
new file mode 100644
index 00000000..61a7b6f6
--- /dev/null
+++ b/pydis_site/apps/resources/resources/vcokltfre_discord_bot_tutorial.yaml
@@ -0,0 +1,14 @@
+description: This tutorial, written by Python Discord staff member vcokltfre,
+ will walk you through all the aspects of creating your own Discord bot,
+ starting from creating the bot user itself.
+name: vcokltfre's Discord Bot Tutorial
+title_url: https://tutorial.vcokltfre.dev/
+tags:
+ topics:
+ - discord bots
+ payment_tiers:
+ - free
+ difficulty:
+ - intermediate
+ type:
+ - tutorial
diff --git a/pydis_site/apps/resources/resources/videos/_category_info.yaml b/pydis_site/apps/resources/resources/videos/_category_info.yaml
deleted file mode 100644
index 8192e021..00000000
--- a/pydis_site/apps/resources/resources/videos/_category_info.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
-description: Excellent Youtube channels with content related to Python.
-name: Videos
diff --git a/pydis_site/apps/resources/resources/videos/python_discord.yaml b/pydis_site/apps/resources/resources/videos/python_discord.yaml
deleted file mode 100644
index 04235b08..00000000
--- a/pydis_site/apps/resources/resources/videos/python_discord.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
-description: It's our channel! We are slowly gathering content here directly related to Python,
- our community and the events we host. Come check us out!
-title_image: https://raw.githubusercontent.com/python-discord/branding/master/logos/logo_banner/logo_site_banner_dark_512.png
-position: 2
-urls:
- - icon: branding/youtube
- url: https://www.youtube.com/pythondiscord
- color: youtube-red
diff --git a/pydis_site/apps/resources/resources/tools/editors/visual_studio_code.yaml b/pydis_site/apps/resources/resources/visual_studio_code.yaml
index e3737ca7..3cf858f8 100644
--- a/pydis_site/apps/resources/resources/tools/editors/visual_studio_code.yaml
+++ b/pydis_site/apps/resources/resources/visual_studio_code.yaml
@@ -1,4 +1,13 @@
description: A fully-featured editor based on Electron, extendable with plugins.
name: Visual Studio Code
title_url: https://code.visualstudio.com/
-position: 1
+tags:
+ topics:
+ - general
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ - intermediate
+ type:
+ - tool
diff --git a/pydis_site/apps/resources/resources/reading/tutorials/wtf_python.yaml b/pydis_site/apps/resources/resources/wtf_python.yaml
index a25a84fd..6d90ba39 100644
--- a/pydis_site/apps/resources/resources/reading/tutorials/wtf_python.yaml
+++ b/pydis_site/apps/resources/resources/wtf_python.yaml
@@ -6,3 +6,13 @@ description: Python, being a beautifully designed high-level and interpreter-bas
name: WTF Python
title_url: https://github.com/satwikkansal/wtfpython
position: 7
+tags:
+ topics:
+ - software design
+ - other
+ payment_tiers:
+ - free
+ difficulty:
+ - intermediate
+ type:
+ - tutorial
diff --git a/pydis_site/apps/resources/templatetags/get_category_icon.py b/pydis_site/apps/resources/templatetags/get_category_icon.py
new file mode 100644
index 00000000..71f1393f
--- /dev/null
+++ b/pydis_site/apps/resources/templatetags/get_category_icon.py
@@ -0,0 +1,39 @@
+from django import template
+
+register = template.Library()
+
+_ICONS = {
+ "Algorithms And Data Structures": "fa-cogs",
+ "Beginner": "fa-play-circle",
+ "Book": "fa-book",
+ "Community": "fa-users",
+ "Course": "fa-chalkboard-teacher",
+ "Data Science": "fa-flask",
+ "Databases": "fa-server",
+ "Discord Bots": "fa-robot",
+ "Free": "fa-first-aid",
+ "Game Development": "fa-gamepad",
+ "General": "fa-book",
+ "Interactive": "fa-mouse-pointer",
+ "Intermediate": "fa-align-center",
+ "Microcontrollers": "fa-microchip",
+ "Other": "fa-question-circle",
+ "Paid": "fa-dollar-sign",
+ "Podcast": "fa-microphone-alt",
+ "Project Ideas": "fa-lightbulb-o",
+ "Software Design": "fa-paint-brush",
+ "Subscription": "fa-credit-card",
+ "Testing": "fa-vial",
+ "Tool": "fa-tools",
+ "Tooling": "fa-toolbox",
+ "Tutorial": "fa-clipboard-list",
+ "User Interface": "fa-desktop",
+ "Video": "fa-video",
+ "Web Development": "fa-wifi",
+}
+
+
+def get_category_icon(name: str) -> str:
+ """Get icon of a specific resource category."""
+ return f'fa {_ICONS[name]}'
diff --git a/pydis_site/apps/resources/templatetags/to_kebabcase.py b/pydis_site/apps/resources/templatetags/to_kebabcase.py
new file mode 100644
index 00000000..41e2ac85
--- /dev/null
+++ b/pydis_site/apps/resources/templatetags/to_kebabcase.py
@@ -0,0 +1,39 @@
+import re
+
+from django import template
+
+REGEX_CONSECUTIVE_NON_LETTERS = r"[^A-Za-z0-9]+"
+register = template.Library()
+
+
+def _to_kebabcase(class_name: str) -> str:
+ """
+ Convert any string to kebab-case.
+
+ For example, convert
+ "__Favorite FROOT¤#/$?is----LeMON???" to
+ "favorite-froot-is-lemon"
+ """
+ # First, make it lowercase, and just remove any apostrophes.
+ # We remove the apostrophes because "wasnt" is better than "wasn-t"
+ class_name = class_name.casefold()
+ class_name = class_name.replace("'", '')
+
+ # Now, replace any non-letter that remains with a dash.
+ # If there are multiple consecutive non-letters, just replace them with a single dash.
+ # my-favorite-class is better than my-favorite------class
+ class_name = re.sub(
+ REGEX_CONSECUTIVE_NON_LETTERS,
+ "-",
+ class_name,
+ )
+
+ # Now we use strip to get rid of any leading or trailing dashes.
+ class_name = class_name.strip("-")
+ return class_name
+
+
+def to_kebabcase(class_name: str) -> str:
+ """Convert a string to kebab-case."""
+ return _to_kebabcase(class_name)
diff --git a/pydis_site/apps/resources/tests/test_to_kebabcase.py b/pydis_site/apps/resources/tests/test_to_kebabcase.py
new file mode 100644
index 00000000..a141143d
--- /dev/null
+++ b/pydis_site/apps/resources/tests/test_to_kebabcase.py
@@ -0,0 +1,19 @@
+from django.test import TestCase
+
+from pydis_site.apps.resources.templatetags.to_kebabcase import _to_kebabcase
+
+
+class TestToKebabcase(TestCase):
+ """Tests for the `as_css_class` template tag."""
+
+ def test_to_kebabcase(self):
+ """Test the to_kebabcase utility and template tag."""
+ weird_input = (
+ "_-_--_A_LEm0n?in&¤'the##trEE£$@€@€@@£is-NOT----QUITE//"
+ "as#good! as one __IN-YOUR|||HaND"
+ )
+
+ self.assertEqual(
+ _to_kebabcase(weird_input),
+ "a-lem0n-in-the-tree-is-not-quite-as-good-as-one-in-your-hand",
+ )
diff --git a/pydis_site/apps/resources/tests/test_views.py b/pydis_site/apps/resources/tests/test_views.py
index 3ad0b958..a2a203ce 100644
--- a/pydis_site/apps/resources/tests/test_views.py
+++ b/pydis_site/apps/resources/tests/test_views.py
@@ -1,5 +1,4 @@
from pathlib import Path
-from unittest.mock import patch
from django.conf import settings
from django.test import TestCase
@@ -17,18 +16,14 @@ class TestResourcesView(TestCase):
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
-
-class TestResourcesListView(TestCase):
- @patch("pydis_site.apps.resources.views.resources_list.RESOURCES_PATH", TESTING_RESOURCES_PATH)
- def test_valid_resource_list_200(self):
- """Check does site return code 200 when visiting valid resource list."""
- url = reverse("resources:resources", args=("testing",))
+ def test_resources_with_valid_argument(self):
+ """Check that you can resolve the resources when passing a valid argument."""
+ url = reverse("resources:index", kwargs={"resource_type": "book"})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
- @patch("pydis_site.apps.resources.views.resources_list.RESOURCES_PATH", TESTING_RESOURCES_PATH)
- def test_invalid_resource_list_404(self):
- """Check does site return code 404 when trying to visit invalid resource list."""
- url = reverse("resources:resources", args=("invalid",))
+ def test_resources_with_invalid_argument(self):
+ """Check that you can resolve the resources when passing an invalid argument."""
+ url = reverse("resources:index", kwargs={"resource_type": "urinal-cake"})
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
diff --git a/pydis_site/apps/resources/tests/testing_resources/testing/_category_info.yaml b/pydis_site/apps/resources/tests/testing_resources/testing/_category_info.yaml
deleted file mode 100644
index bae17ea3..00000000
--- a/pydis_site/apps/resources/tests/testing_resources/testing/_category_info.yaml
+++ /dev/null
@@ -1 +0,0 @@
-name: Testing
diff --git a/pydis_site/apps/resources/tests/testing_resources/testing/foobar/_category_info.yaml b/pydis_site/apps/resources/tests/testing_resources/testing/foobar/_category_info.yaml
deleted file mode 100644
index eaac32d9..00000000
--- a/pydis_site/apps/resources/tests/testing_resources/testing/foobar/_category_info.yaml
+++ /dev/null
@@ -1 +0,0 @@
-name: Foobar
diff --git a/pydis_site/apps/resources/tests/testing_resources/testing/foobar/resource_test.yaml b/pydis_site/apps/resources/tests/testing_resources/testing/foobar/resource_test.yaml
deleted file mode 100644
index 22835090..00000000
--- a/pydis_site/apps/resources/tests/testing_resources/testing/foobar/resource_test.yaml
+++ /dev/null
@@ -1 +0,0 @@
-name: Resource Test
diff --git a/pydis_site/apps/resources/tests/testing_resources/testing/my_resource.yaml b/pydis_site/apps/resources/tests/testing_resources/testing/my_resource.yaml
deleted file mode 100644
index 61df6173..00000000
--- a/pydis_site/apps/resources/tests/testing_resources/testing/my_resource.yaml
+++ /dev/null
@@ -1 +0,0 @@
-name: My Resource
diff --git a/pydis_site/apps/resources/urls.py b/pydis_site/apps/resources/urls.py
index 10eda132..ed24dc99 100644
--- a/pydis_site/apps/resources/urls.py
+++ b/pydis_site/apps/resources/urls.py
@@ -1,25 +1,9 @@
-import typing
-from pathlib import Path
-
from django_distill import distill_path
from pydis_site.apps.resources import views
app_name = "resources"
-
-
-def get_all_resources() -> typing.Iterator[dict[str, str]]:
- """Yield a dict of all resource categories."""
- for category in Path("pydis_site", "apps", "resources", "resources").iterdir():
- yield {"category": category.name}
-
-
urlpatterns = [
- distill_path("", views.ResourcesView.as_view(), name="index"),
- distill_path(
- "<str:category>/",
- views.ResourcesListView.as_view(),
- name="resources",
- distill_func=get_all_resources
- ),
+ distill_path("", views.resources.ResourceView.as_view(), name="index"),
+ distill_path("<resource_type>/", views.resources.ResourceView.as_view(), name="index"),
]
diff --git a/pydis_site/apps/resources/utils.py b/pydis_site/apps/resources/utils.py
deleted file mode 100644
index 1855fc80..00000000
--- a/pydis_site/apps/resources/utils.py
+++ /dev/null
@@ -1,42 +0,0 @@
-import typing as t
-from pathlib import Path
-
-import yaml
-
-
-def get_resources(path: Path) -> t.List[t.Dict]:
- """Loads resource YAMLs from provided path."""
- resources = []
-
- for item in path.iterdir():
- if item.is_file() and item.suffix == ".yaml" and item.name != "_category_info.yaml":
- resources.append(yaml.safe_load(item.read_text()))
-
- return resources
-
-
-def get_subcategories(path: Path) -> t.List[t.Dict]:
- """Loads resources subcategories with their resources by provided path."""
- subcategories = []
-
- for item in path.iterdir():
- if item.is_dir() and item.joinpath("_category_info.yaml").exists():
- subcategories.append({
- "category_info": {
- **yaml.safe_load(
- item.joinpath("_category_info.yaml").read_text()
- ),
- "raw_name": item.name
- },
- "resources": [
- yaml.safe_load(subitem.read_text())
- for subitem in item.iterdir()
- if (
- subitem.is_file()
- and subitem.suffix == ".yaml"
- and subitem.name != "_category_info.yaml"
- )
- ]
- })
-
- return subcategories
diff --git a/pydis_site/apps/resources/views/__init__.py b/pydis_site/apps/resources/views/__init__.py
index 8eb383b5..986f3e10 100644
--- a/pydis_site/apps/resources/views/__init__.py
+++ b/pydis_site/apps/resources/views/__init__.py
@@ -1,4 +1,3 @@
-from .resources import ResourcesView
-from .resources_list import ResourcesListView
+from .resources import ResourceView
-__all__ = ["ResourcesView", "ResourcesListView"]
+__all__ = ["ResourceView"]
diff --git a/pydis_site/apps/resources/views/resources.py b/pydis_site/apps/resources/views/resources.py
index 25ce3e50..2375f722 100644
--- a/pydis_site/apps/resources/views/resources.py
+++ b/pydis_site/apps/resources/views/resources.py
@@ -1,7 +1,126 @@
-from django.views.generic import TemplateView
+import json
+import typing as t
+from pathlib import Path
+import yaml
+from django.core.handlers.wsgi import WSGIRequest
+from django.http import HttpResponse, HttpResponseNotFound
+from django.shortcuts import render
+from django.views import View
-class ResourcesView(TemplateView):
- """View for resources index page."""
+from pydis_site import settings
+from pydis_site.apps.resources.templatetags.to_kebabcase import to_kebabcase
- template_name = "resources/resources.html"
+RESOURCES_PATH = Path(settings.BASE_DIR, "pydis_site", "apps", "resources", "resources")
+
+
+class ResourceView(View):
+ """Our curated list of good learning resources."""
+
+ @staticmethod
+ def _sort_key_disregard_the(tuple_: tuple) -> str:
+ """Sort a tuple by its key alphabetically, disregarding 'the' as a prefix."""
+ name, resource = tuple_
+ name = name.casefold()
+ if name.startswith("the ") or name.startswith("the_"):
+ return name[4:]
+ return name
+
+ def __init__(self, *args, **kwargs):
+ """Set up all the resources."""
+ super().__init__(*args, **kwargs)
+
+ # Load the resources from the yaml files in /resources/
+ self.resources = {
+ path.stem: yaml.safe_load(path.read_text())
+ for path in RESOURCES_PATH.rglob("*.yaml")
+ }
+
+ # Sort the resources alphabetically
+ self.resources = dict(sorted(self.resources.items(), key=self._sort_key_disregard_the))
+
+ # Parse out all current tags
+ resource_tags = {
+ "topics": set(),
+ "payment_tiers": set(),
+ "difficulty": set(),
+ "type": set(),
+ }
+ for resource_name, resource in self.resources.items():
+ css_classes = []
+ for tag_type in resource_tags.keys():
+ # Store the tags into `resource_tags`
+ tags = resource.get("tags", {}).get(tag_type, [])
+ for tag in tags:
+ tag = tag.title()
+ tag = tag.replace("And", "and")
+ resource_tags[tag_type].add(tag)
+
+ # Make a CSS class friendly representation too, while we're already iterating.
+ for tag in tags:
+ css_tag = to_kebabcase(f"{tag_type}-{tag}")
+ css_classes.append(css_tag)
+
+ # Now add the css classes back to the resource, so we can use them in the template.
+ self.resources[resource_name]["css_classes"] = " ".join(css_classes)
+
+ # Set up all the filter checkbox metadata
+ self.filters = {
+ "Difficulty": {
+ "filters": sorted(resource_tags.get("difficulty")),
+ "icon": "fas fa-brain",
+ "hidden": False,
+ },
+ "Type": {
+ "filters": sorted(resource_tags.get("type")),
+ "icon": "fas fa-photo-video",
+ "hidden": False,
+ },
+ "Payment tiers": {
+ "filters": sorted(resource_tags.get("payment_tiers")),
+ "icon": "fas fa-dollar-sign",
+ "hidden": True,
+ },
+ "Topics": {
+ "filters": sorted(resource_tags.get("topics")),
+ "icon": "fas fa-lightbulb",
+ "hidden": True,
+ }
+ }
+
+ # The bottom topic should always be "Other".
+ self.filters["Topics"]["filters"].remove("Other")
+ self.filters["Topics"]["filters"].append("Other")
+
+ # A complete list of valid filter names
+ self.valid_filters = {
+ "topics": [to_kebabcase(topic) for topic in self.filters["Topics"]["filters"]],
+ "payment_tiers": [
+ to_kebabcase(tier) for tier in self.filters["Payment tiers"]["filters"]
+ ],
+ "type": [to_kebabcase(type_) for type_ in self.filters["Type"]["filters"]],
+ "difficulty": [to_kebabcase(tier) for tier in self.filters["Difficulty"]["filters"]],
+ }
+
+ def get(self, request: WSGIRequest, resource_type: t.Optional[str] = None) -> HttpResponse:
+ """List out all the resources, and any filtering options from the URL."""
+ # Add type filtering if the request is made to somewhere like /resources/video.
+ # We also convert all spaces to dashes, so they'll correspond with the filters.
+ if resource_type:
+ dashless_resource_type = resource_type.replace("-", " ")
+
+ if dashless_resource_type.title() not in self.filters["Type"]["filters"]:
+ return HttpResponseNotFound()
+
+ resource_type = resource_type.replace(" ", "-")
+
+ return render(
+ request,
+ template_name="resources/resources.html",
+ context={
+ "resources": self.resources,
+ "filters": self.filters,
+ "valid_filters": json.dumps(self.valid_filters),
+ "resource_type": resource_type,
+ }
+ )
diff --git a/pydis_site/apps/resources/views/resources_list.py b/pydis_site/apps/resources/views/resources_list.py
deleted file mode 100644
index 55f22993..00000000
--- a/pydis_site/apps/resources/views/resources_list.py
+++ /dev/null
@@ -1,39 +0,0 @@
-from pathlib import Path
-from typing import Any, Dict
-
-import yaml
-from django.conf import settings
-from django.http import Http404
-from django.views.generic import TemplateView
-
-from pydis_site.apps.resources.utils import get_resources, get_subcategories
-
-RESOURCES_PATH = Path(settings.BASE_DIR, "pydis_site", "apps", "resources", "resources")
-
-
-class ResourcesListView(TemplateView):
- """Shows specific resources list."""
-
- template_name = "resources/resources_list.html"
-
- def get_context_data(self, **kwargs) -> Dict[str, Any]:
- """Add resources and subcategories data into context."""
- context = super().get_context_data(**kwargs)
-
- resource_path = RESOURCES_PATH / self.kwargs["category"]
- if (
- not resource_path.is_dir()
- or not resource_path.joinpath("_category_info.yaml").exists()
- ):
- raise Http404
-
- context["resources"] = get_resources(resource_path)
- context["subcategories"] = get_subcategories(resource_path)
- context["category_info"] = {
- **yaml.safe_load(
- resource_path.joinpath("_category_info.yaml").read_text()
- ),
- "raw_name": resource_path.name
- }
-
- return context
diff --git a/pydis_site/settings.py b/pydis_site/settings.py
index d38c298b..3b146f2c 100644
--- a/pydis_site/settings.py
+++ b/pydis_site/settings.py
@@ -272,6 +272,7 @@ BULMA_SETTINGS = {
"bulma-dropdown",
"bulma-navbar-burger",
],
+ "fontawesome_token": "ff22cb6f41",
}
# Information about site repository
diff --git a/pydis_site/static/css/base/base.css b/pydis_site/static/css/base/base.css
index f3fe1e44..4b36b7ce 100644
--- a/pydis_site/static/css/base/base.css
+++ b/pydis_site/static/css/base/base.css
@@ -78,12 +78,20 @@ main.site-content {
color: #00000000;
}
+#netcup-logo {
+ padding-left: 15px;
+ background: url(https://www.netcup-wiki.de/static/assets/images/netcup_logo_white.svg) no-repeat center;
+ background-size: 60px;
+ background-position: 0px 3px;
+ color: #00000000;
+}
+
#django-logo {
padding-bottom: 2px;
- background: url(https://static.djangoproject.com/img/logos/django-logo-negative.png) no-repeat center;
- filter: grayscale(1) invert(0.02);
+ background: url(https://static.djangoproject.com/img/logos/django-logo-negative.svg) no-repeat center;
+ filter: grayscale(1) invert(0.09);
background-size: 52px 25.5px;
- background-position: -1px -2px;
+ background-position: -2px -1px;
color: #00000000;
}
@@ -92,6 +100,7 @@ main.site-content {
height: 20px;
background: url(https://bulma.io/images/bulma-logo-white.png) no-repeat center;
background-size: 60px;
+ background-position: 0px 3px;
color: #00000000;
}
diff --git a/pydis_site/static/css/collapsibles.css b/pydis_site/static/css/collapsibles.css
new file mode 100644
index 00000000..1d73fa00
--- /dev/null
+++ b/pydis_site/static/css/collapsibles.css
@@ -0,0 +1,11 @@
+.collapsible {
+ cursor: pointer;
+ width: 100%;
+ border: none;
+ outline: none;
+}
+
+.collapsible-content {
+ transition: max-height 0.3s ease-out;
+ overflow: hidden;
+}
diff --git a/pydis_site/static/css/content/page.css b/pydis_site/static/css/content/page.css
index 2d4bd325..d831f86d 100644
--- a/pydis_site/static/css/content/page.css
+++ b/pydis_site/static/css/content/page.css
@@ -77,16 +77,3 @@ ul.menu-list.toc {
li img {
margin-top: 0.5em;
}
-
-.collapsible {
- cursor: pointer;
- width: 100%;
- border: none;
- outline: none;
-}
-
-.collapsible-content {
- overflow: hidden;
- max-height: 0;
- transition: max-height 0.2s ease-out;
-}
diff --git a/pydis_site/static/css/resources/resources.css b/pydis_site/static/css/resources/resources.css
index cf4cb472..96d06111 100644
--- a/pydis_site/static/css/resources/resources.css
+++ b/pydis_site/static/css/resources/resources.css
@@ -1,29 +1,293 @@
-.box, .tile.is-parent {
- transition: 0.1s ease-out;
+/* Colors for icons */
+i.resource-icon.is-orangered {
+ color: #FE640A;
}
-.box {
- min-height: 15vh;
+i.resource-icon.is-blurple {
+ color: #7289DA;
}
-.tile.is-parent:hover .box {
- box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23);
+i.resource-icon.is-teal {
+ color: #95DBE5;
}
-.tile.is-parent:hover {
- padding: 0.65rem 0.85rem 0.85rem 0.65rem;
- filter: saturate(1.1) brightness(1.1);
+i.resource-icon.is-youtube-red {
+ color: #BB0000;
+}
+i.resource-icon.is-black {
+ color: #2c3334;
+}
+
+/* Colors when icons are hovered */
+i.resource-icon.is-hoverable:hover {
+ filter: brightness(125%);
+}
+i.resource-icon.is-hoverable.is-black:hover {
+ filter: brightness(170%);
+}
+i.resource-icon.is-hoverable.is-teal:hover {
+ filter: brightness(80%);
+}
+
+/* Icon padding */
+.breadcrumb-section {
+ padding: 1rem;
+}
+i.has-icon-padding {
+ padding: 0 10px 25px 0;
+}
+#tab-content p {
+ display: none;
+}
+#tab-content p.is-active {
+display: block;
+}
+
+/* Disable highlighting for all text in the filters. */
+.filter-checkbox,
+.filter-panel label,
+.card-header span {
+ user-select: none
+}
+
+/* Remove pointless margin in panel header */
+#filter-panel-header {
+ margin-bottom: 0;
+}
+
+/* Full width filter cards */
+#resource-filtering-panel .card .collapsible-content .card-content {
+ padding:0
+}
+
+/* Don't round the corners of the collapsibles */
+.filter-category-header {
+ border-radius: 0;
+}
+
+/* Make the checkboxes indent under the filter headers */
+.filter-category-header .card-header .card-header-title {
+ padding-left: 0;
+}
+.filter-panel {
+ padding-left: 1.5rem;
+}
+.filter-checkbox {
+ margin-right: 0.25em !important;
+}
+
+/* Style the search bar */
+#resource-search {
+ margin: 0.25em 0.25em 0 0.25em;
+}
+
+/* Center the 404 div */
+.no-resources-found {
+ display: none;
+ flex-direction: column;
+ align-items: center;
+ margin-top: 1em;
+}
+
+/* Make sure jQuery will use flex when setting `show()` again. */
+.no-resources-found[style*='display: block'] {
+ display: flex !important;
+}
+
+/* By default, we hide the search tag. We'll add it only when there's a search happening. */
+.tag.search-query {
+ display: none;
+ min-width: fit-content;
+ max-width: fit-content;
+ padding-right: 2em;
+}
+.tag.search-query .inner {
+ display: inline-block;
+ padding: 0;
+ max-width: 16.5rem;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ line-height: 2em;
+}
+.tag.search-query i {
+ margin: 0 !important;
+ display: inline-block;
+ line-height: 2em;
+ float: left;
+ padding-right: 1em;
+}
+
+/* Don't allow the tag pool to exceed its parent containers width. */
+#tag-pool {
+ max-width: 100%;
+}
+
+/* Disable clicking on the checkbox itself. */
+/* Instead, we want to let the anchor tag handle clicks. */
+.filter-checkbox {
+ pointer-events: none;
+}
+
+/* Blurple category icons */
+i.is-primary {
+ color: #7289da;
}
-#readingBlock {
- background-image: linear-gradient(141deg, #911eb4 0%, #b631de 71%, #cf4bf7 100%);
+/* A little space above the filter card, please! */
+.filter-tags {
+ padding-bottom: .5em;
}
-#interactiveBlock {
- background-image: linear-gradient(141deg, #d05600 0%, #da722a 71%, #e68846 100%);
+/* Style the close all filters button */
+.close-filters-button {
+ margin-left: auto;
+ display:none;
+}
+.close-filters-button a {
+ height: fit-content;
+ width: fit-content;
+ margin-right: 6px;
+}
+.close-filters-button a i {
+ color: #939bb3;
+}
+.close-filters-button a i:hover {
+ filter: brightness(115%);
+}
+
+/* When hovering title anchors, just make the color a lighter primary, not black. */
+.resource-box a:hover {
+ filter: brightness(120%);
+ color: #7289DA;
+}
+
+/* Set default display to inline-flex, for centering. */
+span.filter-box-tag {
+ display: none;
+ align-items: center;
+ cursor: pointer;
+ user-select: none;
+}
+
+/* Make sure jQuery will use inline-flex when setting `show()` again. */
+span.filter-box-tag[style*='display: block'] {
+ display: inline-flex !important;
+}
+
+/* Make resource tags clickable */
+.resource-tag {
+ cursor: pointer;
+ user-select: none;
+}
+
+/* Give the resource tags a bit of breathing room */
+.resource-tag-container {
+ padding-left: 1.5rem;
+}
+
+/* When hovering tags, brighten them a bit. */
+.resource-tag:hover,
+.filter-box-tag:hover {
+ filter: brightness(95%);
+}
+
+/* Move the x down 1 pixel to align center */
+button.delete {
+ margin-top: 1px;
+}
+
+/* Colors for delete button x's */
+button.delete.is-primary::before,
+button.delete.is-primary::after {
+ background-color: #2a45a2;
+}
+button.delete.is-success::before,
+button.delete.is-success::after {
+ background-color: #2c9659;
+}
+button.delete.is-danger::before,
+button.delete.is-danger::after {
+ background-color: #c32841;
+}
+button.delete.is-info::before,
+button.delete.is-info::after {
+ background-color: #237fbd;
+}
+
+/* Give outlines to active tags */
+span.filter-box-tag,
+span.resource-tag.active,
+.tag.search-query {
+ outline-width: 1px;
+ outline-style: solid;
+}
+
+/* Disable transitions */
+.no-transition {
+ -webkit-transition: none !important;
+ -moz-transition: none !important;
+ -o-transition: none !important;
+ transition: none !important;
+}
+
+/* Make filter tags sparkle when selected! */
+@keyframes glow_success {
+ from { box-shadow: 0 0 2px 2px #aef4af; }
+ 33% { box-shadow: 0 0 2px 2px #87af7a; }
+ 66% { box-shadow: 0 0 2px 2px #9ceaac; }
+ to { box-shadow: 0 0 2px 2px #7cbf64; }
+}
+
+@keyframes glow_primary {
+ from { box-shadow: 0 0 2px 2px #aeb8f3; }
+ 33% { box-shadow: 0 0 2px 2px #909ed9; }
+ 66% { box-shadow: 0 0 2px 2px #6d7ed4; }
+ to { box-shadow: 0 0 2px 2px #6383b3; }
+}
+
+@keyframes glow_danger {
+ from { box-shadow: 0 0 2px 2px #c9495f; }
+ 33% { box-shadow: 0 0 2px 2px #92486f; }
+ 66% { box-shadow: 0 0 2px 2px #d455ba; }
+ to { box-shadow: 0 0 2px 2px #ff8192; }
+}
+@keyframes glow_info {
+ from { box-shadow: 0 0 2px 2px #4592c9; }
+ 33% { box-shadow: 0 0 2px 2px #6196bb; }
+ 66% { box-shadow: 0 0 2px 2px #5adade; }
+ to { box-shadow: 0 0 2px 2px #6bcfdc; }
+}
+
+span.resource-tag.active.is-primary {
+ animation: glow_primary 4s infinite alternate;
+}
+span.resource-tag.active.has-background-danger-light {
+ animation: glow_danger 4s infinite alternate;
+}
+span.resource-tag.active.has-background-success-light {
+ animation: glow_success 4s infinite alternate;
+}
+span.resource-tag.active.has-background-info-light {
+ animation: glow_info 4s infinite alternate;
}
-#communitiesBlock {
- background-image: linear-gradient(141deg, #3b756f 0%, #3a847c 71%, #41948b 100%);
+/* Smaller filter category headers when on mobile */
+@media screen and (max-width: 480px) {
+ .filter-category-header .card-header .card-header-title {
+ font-size: 14px;
+ padding: 0;
+ }
+ .filter-panel {
+ padding-top: 4px;
+ padding-bottom: 4px;
+ }
+ .tag.search-query .inner {
+ max-width: 16.2rem;
+ }
}
-#podcastsBlock {
- background-image: linear-gradient(141deg, #232382 0%, #30309c 71%, #4343ad 100%);
+/* Constrain the width of the filterbox */
+@media screen and (min-width: 769px) {
+ .filtering-column {
+ max-width: 25rem;
+ min-width: 18rem;
+ }
}
diff --git a/pydis_site/static/css/resources/resources_list.css b/pydis_site/static/css/resources/resources_list.css
deleted file mode 100644
index 33129c87..00000000
--- a/pydis_site/static/css/resources/resources_list.css
+++ /dev/null
@@ -1,55 +0,0 @@
-.breadcrumb-section {
- padding: 1rem;
-}
-
-i.resource-icon.is-orangered {
- color: #FE640A;
-}
-
-i.resource-icon.is-orangered:hover {
- color: #fe9840;
-}
-
-i.resource-icon.is-blurple {
- color: #7289DA;
-}
-
-i.resource-icon.is-blurple:hover {
- color: #93a8da;
-}
-
-i.resource-icon.is-teal {
- color: #95DBE5;
-}
-
-i.resource-icon.is-teal:hover {
- color: #a9f5ff;
-}
-
-i.resource-icon.is-youtube-red {
- color: #BB0000;
-}
-
-i.resource-icon.is-youtube-red:hover {
- color: #f80000;
-}
-
-i.resource-icon.is-amazon-orange {
- color: #FF9900;
-}
-
-i.resource-icon.is-amazon-orange:hover {
- color: #ffb71a;
-}
-
-i.resource-icon.is-black {
- color: #000000;
-}
-
-i.resource-icon.is-black {
- color: #191919;
-}
-
-i.has-icon-padding {
- padding: 0 10px 25px 0;
-}
diff --git a/pydis_site/static/images/resources/duck_pond_404.jpg b/pydis_site/static/images/resources/duck_pond_404.jpg
new file mode 100644
index 00000000..29bcf1d6
--- /dev/null
+++ b/pydis_site/static/images/resources/duck_pond_404.jpg
Binary files differ
diff --git a/pydis_site/static/images/sponsors/netcup.png b/pydis_site/static/images/sponsors/netcup.png
new file mode 100644
index 00000000..e5dff196
--- /dev/null
+++ b/pydis_site/static/images/sponsors/netcup.png
Binary files differ
diff --git a/pydis_site/static/js/collapsibles.js b/pydis_site/static/js/collapsibles.js
new file mode 100644
index 00000000..1df0b9fe
--- /dev/null
+++ b/pydis_site/static/js/collapsibles.js
@@ -0,0 +1,67 @@
+/*
+A utility for creating simple collapsible cards.
+
+To see this in action, go to /resources or /pages/guides/pydis-guides/contributing/bot/
+
+// HOW TO USE THIS //
+First, import this file and the corresponding css file into your template.
+
+ <link rel="stylesheet" href="{% static "css/collapsibles.css" %}">
+ <script defer src="{% static "js/collapsibles.js" %}"></script>
+
+Next, you'll need some HTML that these scripts can interact with.
+
+<div class="card">
+ <button type="button" class="card-header collapsible">
+ <span class="card-header-title subtitle is-6 my-2 ml-2">Your headline</span>
+ <span class="card-header-icon">
+ <i class="fas fa-fw fa-angle-down title is-5" aria-hidden="true"></i>
+ </span>
+ </button>
+ <div class="collapsible-content collapsed">
+ <div class="card-content">
+ You can put anything you want here. Lists, more divs, flexboxes, images, whatever.
+ </div>
+ </div>
+</div>
+
+That's it! Collapsing stuff should now work.
+ */
+
+document.addEventListener("DOMContentLoaded", () => {
+ const contentContainers = document.getElementsByClassName("collapsible-content");
+ for (const container of contentContainers) {
+ // Close any collapsibles that are marked as initially collapsed
+ if (container.classList.contains("collapsed")) {
+ container.style.maxHeight = "0px";
+ // Set maxHeight to the size of the container on all other containers.
+ } else {
+ container.style.maxHeight = container.scrollHeight + "px";
+ }
+ }
+
+ // Listen for click events, and collapse or explode
+ const headers = document.getElementsByClassName("collapsible");
+ for (const header of headers) {
+ const content = header.nextElementSibling;
+ const icon = header.querySelector(".card-header-icon i");
+
+ // Any collapsibles that are not initially collapsed needs an icon switch.
+ if (!content.classList.contains("collapsed")) {
+ icon.classList.remove("fas", "fa-angle-down");
+ icon.classList.add("far", "fa-window-minimize");
+ }
+
+ header.addEventListener("click", () => {
+ if (content.style.maxHeight !== "0px"){
+ content.style.maxHeight = "0px";
+ icon.classList.remove("far", "fa-window-minimize");
+ icon.classList.add("fas", "fa-angle-down");
+ } else {
+ content.style.maxHeight = content.scrollHeight + "px";
+ icon.classList.remove("fas", "fa-angle-down");
+ icon.classList.add("far", "fa-window-minimize");
+ }
+ });
+ }
+});
diff --git a/pydis_site/static/js/content/page.js b/pydis_site/static/js/content/page.js
deleted file mode 100644
index 366a033c..00000000
--- a/pydis_site/static/js/content/page.js
+++ /dev/null
@@ -1,13 +0,0 @@
-document.addEventListener("DOMContentLoaded", () => {
- const headers = document.getElementsByClassName("collapsible");
- for (const header of headers) {
- header.addEventListener("click", () => {
- var content = header.nextElementSibling;
- if (content.style.maxHeight){
- content.style.maxHeight = null;
- } else {
- content.style.maxHeight = content.scrollHeight + "px";
- }
- });
- }
-});
diff --git a/pydis_site/static/js/fuzzysort/LICENSE.md b/pydis_site/static/js/fuzzysort/LICENSE.md
new file mode 100644
index 00000000..a3b9d9d7
--- /dev/null
+++ b/pydis_site/static/js/fuzzysort/LICENSE.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 Stephen Kamenar
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/pydis_site/static/js/fuzzysort/fuzzysort.js b/pydis_site/static/js/fuzzysort/fuzzysort.js
new file mode 100644
index 00000000..ba01ae63
--- /dev/null
+++ b/pydis_site/static/js/fuzzysort/fuzzysort.js
@@ -0,0 +1,636 @@
+/*
+ fuzzysort.js https://github.com/farzher/fuzzysort
+ SublimeText-like Fuzzy Search
+
+ fuzzysort.single('fs', 'Fuzzy Search') // {score: -16}
+ fuzzysort.single('test', 'test') // {score: 0}
+ fuzzysort.single('doesnt exist', 'target') // null
+
+ fuzzysort.go('mr', [{file:'Monitor.cpp'}, {file:'MeshRenderer.cpp'}], {key:'file'})
+ // [{score:-18, obj:{file:'MeshRenderer.cpp'}}, {score:-6009, obj:{file:'Monitor.cpp'}}]
+
+ fuzzysort.go('mr', ['Monitor.cpp', 'MeshRenderer.cpp'])
+ // [{score: -18, target: "MeshRenderer.cpp"}, {score: -6009, target: "Monitor.cpp"}]
+
+ fuzzysort.highlight(fuzzysort.single('fs', 'Fuzzy Search'), '<b>', '</b>')
+ // <b>F</b>uzzy <b>S</b>earch
+*/
+
+// UMD (Universal Module Definition) for fuzzysort
+;(function(root, UMD) {
+ if(typeof define === 'function' && define.amd) define([], UMD)
+ else if(typeof module === 'object' && module.exports) module.exports = UMD()
+ else root.fuzzysort = UMD()
+})(this, function UMD() { function fuzzysortNew(instanceOptions) {
+
+ var fuzzysort = {
+
+ single: function(search, target, options) { ;if(search=='farzher')return{target:"farzher was here (^-^*)/",score:0,indexes:[0,1,2,3,4,5,6]}
+ if(!search) return null
+ if(!isObj(search)) search = fuzzysort.getPreparedSearch(search)
+
+ if(!target) return null
+ if(!isObj(target)) target = fuzzysort.getPrepared(target)
+
+ var allowTypo = options && options.allowTypo!==undefined ? options.allowTypo
+ : instanceOptions && instanceOptions.allowTypo!==undefined ? instanceOptions.allowTypo
+ : true
+ var algorithm = allowTypo ? fuzzysort.algorithm : fuzzysort.algorithmNoTypo
+ return algorithm(search, target, search[0])
+ },
+
+ go: function(search, targets, options) { ;if(search=='farzher')return[{target:"farzher was here (^-^*)/",score:0,indexes:[0,1,2,3,4,5,6],obj:targets?targets[0]:null}]
+ if(!search) return noResults
+ search = fuzzysort.prepareSearch(search)
+ var searchLowerCode = search[0]
+
+ var threshold = options && options.threshold || instanceOptions && instanceOptions.threshold || -9007199254740991
+ var limit = options && options.limit || instanceOptions && instanceOptions.limit || 9007199254740991
+ var allowTypo = options && options.allowTypo!==undefined ? options.allowTypo
+ : instanceOptions && instanceOptions.allowTypo!==undefined ? instanceOptions.allowTypo
+ : true
+ var algorithm = allowTypo ? fuzzysort.algorithm : fuzzysort.algorithmNoTypo
+ var resultsLen = 0; var limitedCount = 0
+ var targetsLen = targets.length
+
+ // This code is copy/pasted 3 times for performance reasons [options.keys, options.key, no keys]
+
+ // options.keys
+ if(options && options.keys) {
+ var scoreFn = options.scoreFn || defaultScoreFn
+ var keys = options.keys
+ var keysLen = keys.length
+ for(var i = targetsLen - 1; i >= 0; --i) { var obj = targets[i]
+ var objResults = new Array(keysLen)
+ for (var keyI = keysLen - 1; keyI >= 0; --keyI) {
+ var key = keys[keyI]
+ var target = getValue(obj, key)
+ if(!target) { objResults[keyI] = null; continue }
+ if(!isObj(target)) target = fuzzysort.getPrepared(target)
+
+ objResults[keyI] = algorithm(search, target, searchLowerCode)
+ }
+ objResults.obj = obj // before scoreFn so scoreFn can use it
+ var score = scoreFn(objResults)
+ if(score === null) continue
+ if(score < threshold) continue
+ objResults.score = score
+ if(resultsLen < limit) { q.add(objResults); ++resultsLen }
+ else {
+ ++limitedCount
+ if(score > q.peek().score) q.replaceTop(objResults)
+ }
+ }
+
+ // options.key
+ } else if(options && options.key) {
+ var key = options.key
+ for(var i = targetsLen - 1; i >= 0; --i) { var obj = targets[i]
+ var target = getValue(obj, key)
+ if(!target) continue
+ if(!isObj(target)) target = fuzzysort.getPrepared(target)
+
+ var result = algorithm(search, target, searchLowerCode)
+ if(result === null) continue
+ if(result.score < threshold) continue
+
+ // have to clone result so duplicate targets from different obj can each reference the correct obj
+ result = {target:result.target, _targetLowerCodes:null, _nextBeginningIndexes:null, score:result.score, indexes:result.indexes, obj:obj} // hidden
+
+ if(resultsLen < limit) { q.add(result); ++resultsLen }
+ else {
+ ++limitedCount
+ if(result.score > q.peek().score) q.replaceTop(result)
+ }
+ }
+
+ // no keys
+ } else {
+ for(var i = targetsLen - 1; i >= 0; --i) { var target = targets[i]
+ if(!target) continue
+ if(!isObj(target)) target = fuzzysort.getPrepared(target)
+
+ var result = algorithm(search, target, searchLowerCode)
+ if(result === null) continue
+ if(result.score < threshold) continue
+ if(resultsLen < limit) { q.add(result); ++resultsLen }
+ else {
+ ++limitedCount
+ if(result.score > q.peek().score) q.replaceTop(result)
+ }
+ }
+ }
+
+ if(resultsLen === 0) return noResults
+ var results = new Array(resultsLen)
+ for(var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll()
+ results.total = resultsLen + limitedCount
+ return results
+ },
+
+ goAsync: function(search, targets, options) {
+ var canceled = false
+ var p = new Promise(function(resolve, reject) { ;if(search=='farzher')return resolve([{target:"farzher was here (^-^*)/",score:0,indexes:[0,1,2,3,4,5,6],obj:targets?targets[0]:null}])
+ if(!search) return resolve(noResults)
+ search = fuzzysort.prepareSearch(search)
+ var searchLowerCode = search[0]
+
+ var q = fastpriorityqueue()
+ var iCurrent = targets.length - 1
+ var threshold = options && options.threshold || instanceOptions && instanceOptions.threshold || -9007199254740991
+ var limit = options && options.limit || instanceOptions && instanceOptions.limit || 9007199254740991
+ var allowTypo = options && options.allowTypo!==undefined ? options.allowTypo
+ : instanceOptions && instanceOptions.allowTypo!==undefined ? instanceOptions.allowTypo
+ : true
+ var algorithm = allowTypo ? fuzzysort.algorithm : fuzzysort.algorithmNoTypo
+ var resultsLen = 0; var limitedCount = 0
+ function step() {
+ if(canceled) return reject('canceled')
+
+ var startMs = Date.now()
+
+ // This code is copy/pasted 3 times for performance reasons [options.keys, options.key, no keys]
+
+ // options.keys
+ if(options && options.keys) {
+ var scoreFn = options.scoreFn || defaultScoreFn
+ var keys = options.keys
+ var keysLen = keys.length
+ for(; iCurrent >= 0; --iCurrent) {
+ if(iCurrent%1000/*itemsPerCheck*/ === 0) {
+ if(Date.now() - startMs >= 10/*asyncInterval*/) {
+ isNode?setImmediate(step):setTimeout(step)
+ return
+ }
+ }
+
+ var obj = targets[iCurrent]
+ var objResults = new Array(keysLen)
+ for (var keyI = keysLen - 1; keyI >= 0; --keyI) {
+ var key = keys[keyI]
+ var target = getValue(obj, key)
+ if(!target) { objResults[keyI] = null; continue }
+ if(!isObj(target)) target = fuzzysort.getPrepared(target)
+
+ objResults[keyI] = algorithm(search, target, searchLowerCode)
+ }
+ objResults.obj = obj // before scoreFn so scoreFn can use it
+ var score = scoreFn(objResults)
+ if(score === null) continue
+ if(score < threshold) continue
+ objResults.score = score
+ if(resultsLen < limit) { q.add(objResults); ++resultsLen }
+ else {
+ ++limitedCount
+ if(score > q.peek().score) q.replaceTop(objResults)
+ }
+ }
+
+ // options.key
+ } else if(options && options.key) {
+ var key = options.key
+ for(; iCurrent >= 0; --iCurrent) {
+ if(iCurrent%1000/*itemsPerCheck*/ === 0) {
+ if(Date.now() - startMs >= 10/*asyncInterval*/) {
+ isNode?setImmediate(step):setTimeout(step)
+ return
+ }
+ }
+
+ var obj = targets[iCurrent]
+ var target = getValue(obj, key)
+ if(!target) continue
+ if(!isObj(target)) target = fuzzysort.getPrepared(target)
+
+ var result = algorithm(search, target, searchLowerCode)
+ if(result === null) continue
+ if(result.score < threshold) continue
+
+ // have to clone result so duplicate targets from different obj can each reference the correct obj
+ result = {target:result.target, _targetLowerCodes:null, _nextBeginningIndexes:null, score:result.score, indexes:result.indexes, obj:obj} // hidden
+
+ if(resultsLen < limit) { q.add(result); ++resultsLen }
+ else {
+ ++limitedCount
+ if(result.score > q.peek().score) q.replaceTop(result)
+ }
+ }
+
+ // no keys
+ } else {
+ for(; iCurrent >= 0; --iCurrent) {
+ if(iCurrent%1000/*itemsPerCheck*/ === 0) {
+ if(Date.now() - startMs >= 10/*asyncInterval*/) {
+ isNode?setImmediate(step):setTimeout(step)
+ return
+ }
+ }
+
+ var target = targets[iCurrent]
+ if(!target) continue
+ if(!isObj(target)) target = fuzzysort.getPrepared(target)
+
+ var result = algorithm(search, target, searchLowerCode)
+ if(result === null) continue
+ if(result.score < threshold) continue
+ if(resultsLen < limit) { q.add(result); ++resultsLen }
+ else {
+ ++limitedCount
+ if(result.score > q.peek().score) q.replaceTop(result)
+ }
+ }
+ }
+
+ if(resultsLen === 0) return resolve(noResults)
+ var results = new Array(resultsLen)
+ for(var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll()
+ results.total = resultsLen + limitedCount
+ resolve(results)
+ }
+
+ isNode?setImmediate(step):step() //setTimeout here is too slow
+ })
+ p.cancel = function() { canceled = true }
+ return p
+ },
+
+ highlight: function(result, hOpen, hClose) {
+ if(typeof hOpen == 'function') return fuzzysort.highlightCallback(result, hOpen)
+ if(result === null) return null
+ if(hOpen === undefined) hOpen = '<b>'
+ if(hClose === undefined) hClose = '</b>'
+ var highlighted = ''
+ var matchesIndex = 0
+ var opened = false
+ var target = result.target
+ var targetLen = target.length
+ var matchesBest = result.indexes
+ for(var i = 0; i < targetLen; ++i) { var char = target[i]
+ if(matchesBest[matchesIndex] === i) {
+ ++matchesIndex
+ if(!opened) { opened = true
+ highlighted += hOpen
+ }
+
+ if(matchesIndex === matchesBest.length) {
+ highlighted += char + hClose + target.substr(i+1)
+ break
+ }
+ } else {
+ if(opened) { opened = false
+ highlighted += hClose
+ }
+ }
+ highlighted += char
+ }
+
+ return highlighted
+ },
+ highlightCallback: function(result, cb) {
+ if(result === null) return null
+ var target = result.target
+ var targetLen = target.length
+ var indexes = result.indexes
+ var highlighted = ''
+ var matchI = 0
+ var indexesI = 0
+ var opened = false
+ var result = []
+ for(var i = 0; i < targetLen; ++i) { var char = target[i]
+ if(indexes[indexesI] === i) {
+ ++indexesI
+ if(!opened) { opened = true
+ result.push(highlighted); highlighted = ''
+ }
+
+ if(indexesI === indexes.length) {
+ highlighted += char
+ result.push(cb(highlighted, matchI++)); highlighted = ''
+ result.push(target.substr(i+1))
+ break
+ }
+ } else {
+ if(opened) { opened = false
+ result.push(cb(highlighted, matchI++)); highlighted = ''
+ }
+ }
+ highlighted += char
+ }
+ return result
+ },
+
+ prepare: function(target) {
+ if(!target) return {target: '', _targetLowerCodes: [0/*this 0 doesn't make sense. here because an empty array causes the algorithm to deoptimize and run 50% slower!*/], _nextBeginningIndexes: null, score: null, indexes: null, obj: null} // hidden
+ return {target:target, _targetLowerCodes:fuzzysort.prepareLowerCodes(target), _nextBeginningIndexes:null, score:null, indexes:null, obj:null} // hidden
+ },
+ prepareSlow: function(target) {
+ if(!target) return {target: '', _targetLowerCodes: [0/*this 0 doesn't make sense. here because an empty array causes the algorithm to deoptimize and run 50% slower!*/], _nextBeginningIndexes: null, score: null, indexes: null, obj: null} // hidden
+ return {target:target, _targetLowerCodes:fuzzysort.prepareLowerCodes(target), _nextBeginningIndexes:fuzzysort.prepareNextBeginningIndexes(target), score:null, indexes:null, obj:null} // hidden
+ },
+ prepareSearch: function(search) {
+ if(!search) search = ''
+ return fuzzysort.prepareLowerCodes(search)
+ },
+
+
+
+ // Below this point is only internal code
+ // Below this point is only internal code
+ // Below this point is only internal code
+ // Below this point is only internal code
+
+
+
+ getPrepared: function(target) {
+ if(target.length > 999) return fuzzysort.prepare(target) // don't cache huge targets
+ var targetPrepared = preparedCache.get(target)
+ if(targetPrepared !== undefined) return targetPrepared
+ targetPrepared = fuzzysort.prepare(target)
+ preparedCache.set(target, targetPrepared)
+ return targetPrepared
+ },
+ getPreparedSearch: function(search) {
+ if(search.length > 999) return fuzzysort.prepareSearch(search) // don't cache huge searches
+ var searchPrepared = preparedSearchCache.get(search)
+ if(searchPrepared !== undefined) return searchPrepared
+ searchPrepared = fuzzysort.prepareSearch(search)
+ preparedSearchCache.set(search, searchPrepared)
+ return searchPrepared
+ },
+
+ algorithm: function(searchLowerCodes, prepared, searchLowerCode) {
+ var targetLowerCodes = prepared._targetLowerCodes
+ var searchLen = searchLowerCodes.length
+ var targetLen = targetLowerCodes.length
+ var searchI = 0 // where we at
+ var targetI = 0 // where you at
+ var typoSimpleI = 0
+ var matchesSimpleLen = 0
+
+ // very basic fuzzy match; to remove non-matching targets ASAP!
+ // walk through target. find sequential matches.
+ // if all chars aren't found then exit
+ for(;;) {
+ var isMatch = searchLowerCode === targetLowerCodes[targetI]
+ if(isMatch) {
+ matchesSimple[matchesSimpleLen++] = targetI
+ ++searchI; if(searchI === searchLen) break
+ searchLowerCode = searchLowerCodes[typoSimpleI===0?searchI : (typoSimpleI===searchI?searchI+1 : (typoSimpleI===searchI-1?searchI-1 : searchI))]
+ }
+
+ ++targetI; if(targetI >= targetLen) { // Failed to find searchI
+ // Check for typo or exit
+ // we go as far as possible before trying to transpose
+ // then we transpose backwards until we reach the beginning
+ for(;;) {
+ if(searchI <= 1) return null // not allowed to transpose first char
+ if(typoSimpleI === 0) { // we haven't tried to transpose yet
+ --searchI
+ var searchLowerCodeNew = searchLowerCodes[searchI]
+ if(searchLowerCode === searchLowerCodeNew) continue // doesn't make sense to transpose a repeat char
+ typoSimpleI = searchI
+ } else {
+ if(typoSimpleI === 1) return null // reached the end of the line for transposing
+ --typoSimpleI
+ searchI = typoSimpleI
+ searchLowerCode = searchLowerCodes[searchI + 1]
+ var searchLowerCodeNew = searchLowerCodes[searchI]
+ if(searchLowerCode === searchLowerCodeNew) continue // doesn't make sense to transpose a repeat char
+ }
+ matchesSimpleLen = searchI
+ targetI = matchesSimple[matchesSimpleLen - 1] + 1
+ break
+ }
+ }
+ }
+
+ var searchI = 0
+ var typoStrictI = 0
+ var successStrict = false
+ var matchesStrictLen = 0
+
+ var nextBeginningIndexes = prepared._nextBeginningIndexes
+ if(nextBeginningIndexes === null) nextBeginningIndexes = prepared._nextBeginningIndexes = fuzzysort.prepareNextBeginningIndexes(prepared.target)
+ var firstPossibleI = targetI = matchesSimple[0]===0 ? 0 : nextBeginningIndexes[matchesSimple[0]-1]
+
+ // Our target string successfully matched all characters in sequence!
+ // Let's try a more advanced and strict test to improve the score
+ // only count it as a match if it's consecutive or a beginning character!
+ if(targetI !== targetLen) for(;;) {
+ if(targetI >= targetLen) {
+ // We failed to find a good spot for this search char, go back to the previous search char and force it forward
+ if(searchI <= 0) { // We failed to push chars forward for a better match
+ // transpose, starting from the beginning
+ ++typoStrictI; if(typoStrictI > searchLen-2) break
+ if(searchLowerCodes[typoStrictI] === searchLowerCodes[typoStrictI+1]) continue // doesn't make sense to transpose a repeat char
+ targetI = firstPossibleI
+ continue
+ }
+
+ --searchI
+ var lastMatch = matchesStrict[--matchesStrictLen]
+ targetI = nextBeginningIndexes[lastMatch]
+
+ } else {
+ var isMatch = searchLowerCodes[typoStrictI===0?searchI : (typoStrictI===searchI?searchI+1 : (typoStrictI===searchI-1?searchI-1 : searchI))] === targetLowerCodes[targetI]
+ if(isMatch) {
+ matchesStrict[matchesStrictLen++] = targetI
+ ++searchI; if(searchI === searchLen) { successStrict = true; break }
+ ++targetI
+ } else {
+ targetI = nextBeginningIndexes[targetI]
+ }
+ }
+ }
+
+ { // tally up the score & keep track of matches for highlighting later
+ if(successStrict) { var matchesBest = matchesStrict; var matchesBestLen = matchesStrictLen }
+ else { var matchesBest = matchesSimple; var matchesBestLen = matchesSimpleLen }
+ var score = 0
+ var lastTargetI = -1
+ for(var i = 0; i < searchLen; ++i) { var targetI = matchesBest[i]
+ // score only goes down if they're not consecutive
+ if(lastTargetI !== targetI - 1) score -= targetI
+ lastTargetI = targetI
+ }
+ if(!successStrict) {
+ score *= 1000
+ if(typoSimpleI !== 0) score += -20/*typoPenalty*/
+ } else {
+ if(typoStrictI !== 0) score += -20/*typoPenalty*/
+ }
+ score -= targetLen - searchLen
+ prepared.score = score
+ prepared.indexes = new Array(matchesBestLen); for(var i = matchesBestLen - 1; i >= 0; --i) prepared.indexes[i] = matchesBest[i]
+
+ return prepared
+ }
+ },
+
+ algorithmNoTypo: function(searchLowerCodes, prepared, searchLowerCode) {
+ var targetLowerCodes = prepared._targetLowerCodes
+ var searchLen = searchLowerCodes.length
+ var targetLen = targetLowerCodes.length
+ var searchI = 0 // where we at
+ var targetI = 0 // where you at
+ var matchesSimpleLen = 0
+
+ // very basic fuzzy match; to remove non-matching targets ASAP!
+ // walk through target. find sequential matches.
+ // if all chars aren't found then exit
+ for(;;) {
+ var isMatch = searchLowerCode === targetLowerCodes[targetI]
+ if(isMatch) {
+ matchesSimple[matchesSimpleLen++] = targetI
+ ++searchI; if(searchI === searchLen) break
+ searchLowerCode = searchLowerCodes[searchI]
+ }
+ ++targetI; if(targetI >= targetLen) return null // Failed to find searchI
+ }
+
+ var searchI = 0
+ var successStrict = false
+ var matchesStrictLen = 0
+
+ var nextBeginningIndexes = prepared._nextBeginningIndexes
+ if(nextBeginningIndexes === null) nextBeginningIndexes = prepared._nextBeginningIndexes = fuzzysort.prepareNextBeginningIndexes(prepared.target)
+ var firstPossibleI = targetI = matchesSimple[0]===0 ? 0 : nextBeginningIndexes[matchesSimple[0]-1]
+
+ // Our target string successfully matched all characters in sequence!
+ // Let's try a more advanced and strict test to improve the score
+ // only count it as a match if it's consecutive or a beginning character!
+ if(targetI !== targetLen) for(;;) {
+ if(targetI >= targetLen) {
+ // We failed to find a good spot for this search char, go back to the previous search char and force it forward
+ if(searchI <= 0) break // We failed to push chars forward for a better match
+
+ --searchI
+ var lastMatch = matchesStrict[--matchesStrictLen]
+ targetI = nextBeginningIndexes[lastMatch]
+
+ } else {
+ var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI]
+ if(isMatch) {
+ matchesStrict[matchesStrictLen++] = targetI
+ ++searchI; if(searchI === searchLen) { successStrict = true; break }
+ ++targetI
+ } else {
+ targetI = nextBeginningIndexes[targetI]
+ }
+ }
+ }
+
+ { // tally up the score & keep track of matches for highlighting later
+ if(successStrict) { var matchesBest = matchesStrict; var matchesBestLen = matchesStrictLen }
+ else { var matchesBest = matchesSimple; var matchesBestLen = matchesSimpleLen }
+ var score = 0
+ var lastTargetI = -1
+ for(var i = 0; i < searchLen; ++i) { var targetI = matchesBest[i]
+ // score only goes down if they're not consecutive
+ if(lastTargetI !== targetI - 1) score -= targetI
+ lastTargetI = targetI
+ }
+ if(!successStrict) score *= 1000
+ score -= targetLen - searchLen
+ prepared.score = score
+ prepared.indexes = new Array(matchesBestLen); for(var i = matchesBestLen - 1; i >= 0; --i) prepared.indexes[i] = matchesBest[i]
+
+ return prepared
+ }
+ },
+
+ prepareLowerCodes: function(str) {
+ var strLen = str.length
+ var lowerCodes = [] // new Array(strLen) sparse array is too slow
+ var lower = str.toLowerCase()
+ for(var i = 0; i < strLen; ++i) lowerCodes[i] = lower.charCodeAt(i)
+ return lowerCodes
+ },
+ prepareBeginningIndexes: function(target) {
+ var targetLen = target.length
+ var beginningIndexes = []; var beginningIndexesLen = 0
+ var wasUpper = false
+ var wasAlphanum = false
+ for(var i = 0; i < targetLen; ++i) {
+ var targetCode = target.charCodeAt(i)
+ var isUpper = targetCode>=65&&targetCode<=90
+ var isAlphanum = isUpper || targetCode>=97&&targetCode<=122 || targetCode>=48&&targetCode<=57
+ var isBeginning = isUpper && !wasUpper || !wasAlphanum || !isAlphanum
+ wasUpper = isUpper
+ wasAlphanum = isAlphanum
+ if(isBeginning) beginningIndexes[beginningIndexesLen++] = i
+ }
+ return beginningIndexes
+ },
+ prepareNextBeginningIndexes: function(target) {
+ var targetLen = target.length
+ var beginningIndexes = fuzzysort.prepareBeginningIndexes(target)
+ var nextBeginningIndexes = [] // new Array(targetLen) sparse array is too slow
+ var lastIsBeginning = beginningIndexes[0]
+ var lastIsBeginningI = 0
+ for(var i = 0; i < targetLen; ++i) {
+ if(lastIsBeginning > i) {
+ nextBeginningIndexes[i] = lastIsBeginning
+ } else {
+ lastIsBeginning = beginningIndexes[++lastIsBeginningI]
+ nextBeginningIndexes[i] = lastIsBeginning===undefined ? targetLen : lastIsBeginning
+ }
+ }
+ return nextBeginningIndexes
+ },
+
+ cleanup: cleanup,
+ new: fuzzysortNew,
+ }
+ return fuzzysort
+} // fuzzysortNew
+
+// This stuff is outside fuzzysortNew, because it's shared with instances of fuzzysort.new()
+var isNode = typeof require !== 'undefined' && typeof window === 'undefined'
+var MyMap = Map||function(){var s=Object.create(null);this.get=function(k){return s[k]};this.set=function(k,val){s[k]=val;return this};this.clear=function(){s=Object.create(null)}}
+var preparedCache = new MyMap()
+var preparedSearchCache = new MyMap()
+var noResults = []; noResults.total = 0
+var matchesSimple = []; var matchesStrict = []
+function cleanup() { preparedCache.clear(); preparedSearchCache.clear(); matchesSimple = []; matchesStrict = [] }
+function defaultScoreFn(a) {
+ var max = -9007199254740991
+ for (var i = a.length - 1; i >= 0; --i) {
+ var result = a[i]; if(result === null) continue
+ var score = result.score
+ if(score > max) max = score
+ }
+ if(max === -9007199254740991) return null
+ return max
+}
+
+// prop = 'key' 2.5ms optimized for this case, seems to be about as fast as direct obj[prop]
+// prop = 'key1.key2' 10ms
+// prop = ['key1', 'key2'] 27ms
+function getValue(obj, prop) {
+ var tmp = obj[prop]; if(tmp !== undefined) return tmp
+ var segs = prop
+ if(!Array.isArray(prop)) segs = prop.split('.')
+ var len = segs.length
+ var i = -1
+ while (obj && (++i < len)) obj = obj[segs[i]]
+ return obj
+}
+
+function isObj(x) { return typeof x === 'object' } // faster as a function
+
+// Hacked version of https://github.com/lemire/FastPriorityQueue.js
+var fastpriorityqueue=function(){var r=[],o=0,e={};function n(){for(var e=0,n=r[e],c=1;c<o;){var f=c+1;e=c,f<o&&r[f].score<r[c].score&&(e=f),r[e-1>>1]=r[e],c=1+(e<<1)}for(var a=e-1>>1;e>0&&n.score<r[a].score;a=(e=a)-1>>1)r[e]=r[a];r[e]=n}return e.add=function(e){var n=o;r[o++]=e;for(var c=n-1>>1;n>0&&e.score<r[c].score;c=(n=c)-1>>1)r[n]=r[c];r[n]=e},e.poll=function(){if(0!==o){var e=r[0];return r[0]=r[--o],n(),e}},e.peek=function(e){if(0!==o)return r[0]},e.replaceTop=function(o){r[0]=o,n()},e};
+var q = fastpriorityqueue() // reuse this, except for async, it needs to make its own
+
+return fuzzysortNew()
+}) // UMD
+
+// TODO: (performance) wasm version!?
+// TODO: (performance) threads?
+// TODO: (performance) avoid cache misses
+// TODO: (performance) preparedCache is a memory leak
+// TODO: (like sublime) backslash === forwardslash
+// TODO: (like sublime) spaces: "a b" should do 2 searches 1 for a and 1 for b
+// TODO: (scoring) garbage in targets that allows most searches to strict match need a penality
+// TODO: (performance) idk if allowTypo is optimized
diff --git a/pydis_site/static/js/resources/resources.js b/pydis_site/static/js/resources/resources.js
new file mode 100644
index 00000000..d6cc8128
--- /dev/null
+++ b/pydis_site/static/js/resources/resources.js
@@ -0,0 +1,367 @@
+"use strict";
+
+// Filters that are currently selected
+var activeFilters = {
+ topics: [],
+ type: [],
+ "payment-tiers": [],
+ difficulty: []
+};
+
+// Options for fuzzysort
+const fuzzysortOptions = {
+ allowTypo: true, // Allow our users to make typos
+ titleThreshold: -10000, // The threshold for the fuzziness on title matches. Closer to 0 is stricter.
+ descriptionThreshold: -500, // The threshold for the fuzziness on description matches.
+};
+
+/* Add a filter, and update the UI */
+function addFilter(filterName, filterItem) {
+ var filterIndex = activeFilters[filterName].indexOf(filterItem);
+ if (filterIndex === -1) {
+ activeFilters[filterName].push(filterItem);
+ }
+ updateUI();
+}
+
+/* Remove all filters, and update the UI */
+function removeAllFilters() {
+ activeFilters = {
+ topics: [],
+ type: [],
+ "payment-tiers": [],
+ difficulty: []
+ };
+ $("#resource-search input").val("");
+ updateUI();
+}
+
+/* Remove a filter, and update the UI */
+function removeFilter(filterName, filterItem) {
+ var filterIndex = activeFilters[filterName].indexOf(filterItem);
+ if (filterIndex !== -1) {
+ activeFilters[filterName].splice(filterIndex, 1);
+ }
+ updateUI();
+}
+
+/* Check if there are no filters */
+function noFilters() {
+ return (
+ activeFilters.topics.length === 0 &&
+ activeFilters.type.length === 0 &&
+ activeFilters["payment-tiers"].length === 0 &&
+ activeFilters.difficulty.length === 0
+ );
+}
+
+/* Get the params out of the URL and use them. This is run when the page loads. */
+function deserializeURLParams() {
+ let searchParams = new window.URLSearchParams(window.location.search);
+
+ // Add the search query to the search bar.
+ if (searchParams.has("search")) {
+ let searchQuery = searchParams.get("search");
+ $("#resource-search input").val(searchQuery);
+ $(".close-filters-button").show();
+ }
+
+ // Work through the parameters and add them to the filter object
+ $.each(Object.keys(activeFilters), function(_, filterType) {
+ let paramFilterContent = searchParams.get(filterType);
+
+ if (paramFilterContent !== null) {
+ // We use split here because we always want an array, not a string.
+ let paramFilterArray = paramFilterContent.split(",");
+
+ // Update the corresponding filter UI, so it reflects the internal state.
+ let filterAdded = false;
+ $(paramFilterArray).each(function(_, filter) {
+ // Catch special cases.
+ if (String(filter) === "rickroll" && filterType === "type") {
+ window.location.href = "https://www.youtube.com/watch?v=dQw4w9WgXcQ";
+ } else if (String(filter) === "sneakers" && filterType === "topics") {
+ window.location.href = "https://www.youtube.com/watch?v=NNZscmNE9QI";
+
+ // If the filter is valid, mirror it to the UI.
+ } else if (validFilters.hasOwnProperty(filterType) && validFilters[filterType].includes(String(filter))) {
+ let checkbox = $(`.filter-checkbox[data-filter-name='${filterType}'][data-filter-item='${filter}']`);
+ let filterTag = $(`.filter-box-tag[data-filter-name='${filterType}'][data-filter-item='${filter}']`);
+ let resourceTags = $(`.resource-tag[data-filter-name='${filterType}'][data-filter-item='${filter}']`);
+ checkbox.prop("checked", true);
+ filterTag.show();
+ resourceTags.addClass("active");
+ activeFilters[filterType].push(filter);
+ filterAdded = true;
+ }
+ });
+
+ // Ditch all the params from the URL, and recalculate the URL params
+ updateURL();
+
+ // If we've added a filter, hide stuff
+ if (filterAdded) {
+ $(".no-tags-selected.tag").hide();
+ $(".close-filters-button").show();
+ }
+ }
+ });
+}
+
+/* Show or hide the duckies, depending on whether or not there are any resources visible. */
+function updateDuckies() {
+ let visibleResources = Boolean($(".resource-box:visible").length);
+ if (!visibleResources) {
+ $(".no-resources-found").show();
+ } else {
+ $(".no-resources-found").hide();
+ }
+}
+
+
+/* Update the URL with new parameters */
+function updateURL() {
+ let searchQuery = $("#resource-search input").val();
+
+ // If there's no active filtering parameters, we can return early.
+ if (noFilters() && searchQuery.length === 0) {
+ window.history.replaceState(null, document.title, './');
+ return;
+ }
+
+ // Iterate through and get rid of empty ones
+ let searchParams = new URLSearchParams(activeFilters);
+ $.each(activeFilters, function(filterType, filters) {
+ if (filters.length === 0) {
+ searchParams.delete(filterType);
+ }
+ });
+
+ // Add the search query, if necessary.
+ if (searchQuery.length > 0) {
+ searchParams.set("search", searchQuery);
+ }
+
+ // Now update the URL
+ window.history.replaceState(null, document.title, `?${searchParams.toString()}`);
+}
+
+/* Apply search terms */
+function filterBySearch(resourceItems) {
+ let searchQuery = $("#resource-search input").val();
+
+ /* Show and update the tag if there's a search query */
+ if (searchQuery) {
+ let tag = $(".tag.search-query");
+ let tagText = $(".tag.search-query span");
+ tagText.text(`Search: ${searchQuery}`);
+ tag.show();
+ $(".close-filters-button").show();
+ }
+
+ resourceItems.filter(function() {
+ // Get the resource title and description
+ let title = $(this).attr("data-resource-name");
+ let description = $(this).find("p").text();
+
+ // Run a fuzzy search. Does the title or description match the query?
+ let titleMatch = fuzzysort.single(searchQuery, title, fuzzysortOptions);
+ titleMatch = Boolean(titleMatch) && titleMatch.score > fuzzysortOptions.titleThreshold;
+
+ let descriptionMatch = fuzzysort.single(searchQuery, description, fuzzysortOptions);
+ descriptionMatch = Boolean(descriptionMatch) && descriptionMatch.score > fuzzysortOptions.descriptionThreshold;
+
+ return titleMatch || descriptionMatch;
+ }).show();
+}
+
+/* Update the resources to match 'active_filters' */
+function updateUI() {
+ let resources = $('.resource-box');
+ let filterTags = $('.filter-box-tag');
+ let resourceTags = $('.resource-tag');
+ let noTagsSelected = $(".no-tags-selected.tag");
+ let closeFiltersButton = $(".close-filters-button");
+ let searchQuery = $("#resource-search input").val();
+ let searchTag = $(".tag.search-query");
+
+ // Update the URL to match the new filters.
+ updateURL();
+
+ // If there's nothing in the filters, we can return early.
+ if (noFilters()) {
+ // If we have a searchQuery, we need to run all resources through a search.
+ if (searchQuery.length > 0) {
+ resources.hide();
+ noTagsSelected.hide();
+ filterBySearch(resources);
+ } else {
+ resources.show();
+ noTagsSelected.show();
+ closeFiltersButton.hide();
+ $(".tag.search-query").hide();
+ }
+
+ filterTags.hide();
+ resourceTags.removeClass("active");
+ $(`.filter-checkbox:checked`).prop("checked", false);
+ updateDuckies();
+
+ return;
+ } else {
+ // Hide everything
+ $('.filter-box-tag').hide();
+ $('.resource-tag').removeClass("active");
+ noTagsSelected.show();
+ closeFiltersButton.hide();
+
+ // Now conditionally show the stuff we want
+ $.each(activeFilters, function(filterType, filters) {
+ $.each(filters, function(index, filter) {
+ // Show a corresponding filter box tag
+ $(`.filter-box-tag[data-filter-name=${filterType}][data-filter-item=${filter}]`).show();
+
+ // Make corresponding resource tags active
+ $(`.resource-tag[data-filter-name=${filterType}][data-filter-item=${filter}]`).addClass("active");
+
+ // Hide the "No filters selected" tag.
+ noTagsSelected.hide();
+
+ // Show the close filters button
+ closeFiltersButton.show();
+ });
+ });
+ }
+
+ // Otherwise, hide everything and then filter the resources to decide what to show.
+ resources.hide();
+ let filteredResources = resources.filter(function() {
+ let validation = {
+ topics: false,
+ type: false,
+ 'payment-tiers': false,
+ difficulty: false
+ };
+ let resourceBox = $(this);
+
+ // Validate the filters
+ $.each(activeFilters, function(filterType, filters) {
+ // If the filter list is empty, this passes validation.
+ if (filters.length === 0) {
+ validation[filterType] = true;
+ return;
+ }
+
+ // Otherwise, we need to check if one of the classes exist.
+ $.each(filters, function(index, filter) {
+ if (resourceBox.hasClass(`${filterType}-${filter}`)) {
+ validation[filterType] = true;
+ }
+ });
+ });
+
+ // If validation passes, show the resource.
+ if (Object.values(validation).every(Boolean)) {
+ return true;
+ } else {
+ return false;
+ }
+ });
+
+ // Run the items we've found through the search filter, if necessary.
+ if (searchQuery.length > 0) {
+ filterBySearch(filteredResources);
+ } else {
+ filteredResources.show();
+ searchTag.hide();
+ }
+
+ // Gotta update those duckies!
+ updateDuckies();
+}
+
+// Executed when the page has finished loading.
+document.addEventListener("DOMContentLoaded", function () {
+ /* Check if the user has navigated to one of the old resource pages,
+ like pydis.com/resources/communities. In this case, we'll rewrite
+ the URL before we do anything else. */
+ let resourceTypeInput = $("#resource-type-input").val();
+ if (resourceTypeInput !== "None") {
+ window.history.replaceState(null, document.title, `../?type=${resourceTypeInput}`);
+ }
+
+ // Update the filters on page load to reflect URL parameters.
+ $('.filter-box-tag').hide();
+ deserializeURLParams();
+ updateUI();
+
+ // If this is a mobile device, collapse all the categories to win back some screen real estate.
+ if (screen.width < 480) {
+ let categoryHeaders = $(".filter-category-header .collapsible-content");
+ let icons = $('.filter-category-header button .card-header-icon i');
+ categoryHeaders.addClass("no-transition collapsed");
+ icons.removeClass(["far", "fa-window-minimize"]);
+ icons.addClass(["fas", "fa-angle-down"]);
+
+ // Wait 10ms before removing this class, or else the transition will animate due to a race condition.
+ setTimeout(() => { categoryHeaders.removeClass("no-transition"); }, 10);
+ }
+
+ // When you type into the search bar, trigger an UI update.
+ $("#resource-search input").on("input", function() {
+ updateUI();
+ });
+
+ // If you click on the div surrounding the filter checkbox, it clicks the corresponding checkbox.
+ $('.filter-panel').on("click",function(event) {
+ let hitsCheckbox = Boolean(String(event.target));
+
+ if (!hitsCheckbox) {
+ let checkbox = $(this).find(".filter-checkbox");
+ checkbox.prop("checked", !checkbox.prop("checked"));
+ checkbox.trigger("change");
+ }
+ });
+
+ // If you click on one of the tags in the filter box, it unchecks the corresponding checkbox.
+ $('.filter-box-tag').on("click", function() {
+ let filterItem = this.dataset.filterItem;
+ let filterName = this.dataset.filterName;
+ let checkbox = $(`.filter-checkbox[data-filter-name='${filterName}'][data-filter-item='${filterItem}']`);
+
+ removeFilter(filterName, filterItem);
+ checkbox.prop("checked", false);
+ });
+
+ // If you click on one of the tags in the resource cards, it clicks the corresponding checkbox.
+ $('.resource-tag').on("click", function() {
+ let filterItem = this.dataset.filterItem;
+ let filterName = this.dataset.filterName;
+ let checkbox = $(`.filter-checkbox[data-filter-name='${filterName}'][data-filter-item='${filterItem}']`);
+
+ if (!$(this).hasClass("active")) {
+ addFilter(filterName, filterItem);
+ checkbox.prop("checked", true);
+ } else {
+ removeFilter(filterName, filterItem);
+ checkbox.prop("checked", false);
+ }
+ });
+
+ // When you click the little gray x, remove all filters.
+ $(".close-filters-button").on("click", function() {
+ removeAllFilters();
+ });
+
+ // When checkboxes are toggled, trigger a filter update.
+ $('.filter-checkbox').on("change", function (event) {
+ let filterItem = this.dataset.filterItem;
+ let filterName = this.dataset.filterName;
+
+ if (this.checked && !activeFilters[filterName].includes(filterItem)) {
+ addFilter(filterName, filterItem);
+ } else if (!this.checked && activeFilters[filterName].includes(filterItem)) {
+ removeFilter(filterName, filterItem);
+ }
+ });
+});
diff --git a/pydis_site/templates/base/base.html b/pydis_site/templates/base/base.html
index 906fc577..b7322f12 100644
--- a/pydis_site/templates/base/base.html
+++ b/pydis_site/templates/base/base.html
@@ -24,10 +24,7 @@
<title>Python Discord | {% block title %}Website{% endblock %}</title>
{% bulma %}
-
- {# Font-awesome here is defined explicitly so that we can have Pro #}
- <script src="https://kit.fontawesome.com/ae6a3152d8.js"></script>
-
+ {% font_awesome %}
<link rel="stylesheet" href="{% static "css/base/base.css" %}">
{% block head %}{% endblock %}
diff --git a/pydis_site/templates/base/footer.html b/pydis_site/templates/base/footer.html
index bca43b5d..0bc93578 100644
--- a/pydis_site/templates/base/footer.html
+++ b/pydis_site/templates/base/footer.html
@@ -1,7 +1,7 @@
<footer class="footer has-background-dark has-text-light">
<div class="content has-text-centered">
<p>
- Powered by <a href="https://www.linode.com/?r=3bc18ce876ff43ea31f201b91e8e119c9753f085"><span id="linode-logo">Linode</span></a><br>Built with <a href="https://www.djangoproject.com/"><span id="django-logo">django</span></a> and <a href="https://bulma.io"><span id="bulma-logo">Bulma</span></a> <br/> &copy; {% now "Y" %} <span id="pydis-text">Python Discord</span>
+ Powered by <a href="https://www.linode.com/?r=3bc18ce876ff43ea31f201b91e8e119c9753f085"><span id="linode-logo">Linode</span></a> and <a href="https://www.netcup.eu/"><span id="netcup-logo">netcup</span></a><br>Built with <a href="https://www.djangoproject.com/"><span id="django-logo">django</span></a> and <a href="https://bulma.io"><span id="bulma-logo">Bulma</span></a> <br/> &copy; {% now "Y" %} <span id="pydis-text">Python Discord</span>
</p>
</div>
</footer>
diff --git a/pydis_site/templates/base/navbar.html b/pydis_site/templates/base/navbar.html
index 4b68dd6c..d7fb4f4c 100644
--- a/pydis_site/templates/base/navbar.html
+++ b/pydis_site/templates/base/navbar.html
@@ -67,9 +67,6 @@
<a class="navbar-item" href="{% url "resources:index" %}">
Resources
</a>
- <a class="navbar-item" href="{% url "resources:resources" category="tools" %}">
- Tools
- </a>
<a class="navbar-item" href="{% url "events:index" %}">
Events
</a>
@@ -79,6 +76,9 @@
<a class="navbar-item" href="{% url "content:page_category" location="frequently-asked-questions" %}">
FAQ
</a>
+ <a class="navbar-item" href="{% url "content:page_category" location="guides" %}">
+ Guides
+ </a>
<a class="navbar-item" href="{% url 'home:timeline' %}">
Timeline
</a>
diff --git a/pydis_site/templates/content/base.html b/pydis_site/templates/content/base.html
index 00f4fce4..4a19a275 100644
--- a/pydis_site/templates/content/base.html
+++ b/pydis_site/templates/content/base.html
@@ -7,7 +7,8 @@
<meta property="og:type" content="website" />
<meta property="og:description" content="{{ page_description }}" />
<link rel="stylesheet" href="{% static "css/content/page.css" %}">
- <script src="{% static "js/content/page.js" %}"></script>
+ <link rel="stylesheet" href="{% static "css/collapsibles.css" %}">
+ <script src="{% static "js/collapsibles.js" %}"></script>
{% endblock %}
{% block content %}
diff --git a/pydis_site/templates/content/dropdown.html b/pydis_site/templates/content/dropdown.html
index d81e29dc..13c89c68 100644
--- a/pydis_site/templates/content/dropdown.html
+++ b/pydis_site/templates/content/dropdown.html
@@ -1,4 +1,4 @@
-<div class="dropdown is-pulled-right is-right" id="dropdown">
+<div class="dropdown is-pulled-right is-right" id="dropdown" style="z-index: 1">
<div class="dropdown-trigger">
<a aria-haspopup="true" aria-controls="subarticle-menu">
<span>Sub-Articles</span>
diff --git a/pydis_site/templates/home/index.html b/pydis_site/templates/home/index.html
index 985ccae1..c7350cac 100644
--- a/pydis_site/templates/home/index.html
+++ b/pydis_site/templates/home/index.html
@@ -173,6 +173,9 @@
Sponsors
</h1>
<div class="columns is-mobile is-multiline">
+ <a href="https://www.netcup.eu/" class="column is-narrow">
+ <img src="{% static "images/sponsors/netcup.png" %}" alt="netcup"/>
+ </a>
<a href="https://www.linode.com/?r=3bc18ce876ff43ea31f201b91e8e119c9753f085" class="column is-narrow">
<img src="{% static "images/sponsors/linode.png" %}" alt="Linode"/>
</a>
diff --git a/pydis_site/templates/resources/resource_box.html b/pydis_site/templates/resources/resource_box.html
index af7c8d65..5ca46296 100644
--- a/pydis_site/templates/resources/resource_box.html
+++ b/pydis_site/templates/resources/resource_box.html
@@ -1,6 +1,8 @@
{% load as_icon %}
+{% load to_kebabcase %}
+{% load get_category_icon %}
-<div class="box" style="max-width: 800px;">
+<div class="box resource-box {{ resource.css_classes }}" data-resource-name="{{ resource.name }}">
{% if 'title_url' in resource %}
<a href="{{ resource.title_url }}">
{% include "resources/resource_box_header.html" %}
@@ -9,14 +11,69 @@
{% include "resources/resource_box_header.html" %}
{% endif %}
- <p class="is-italic">{{ resource.description|safe }}</p>
+ <p>{{ resource.description|safe }}</p>
- {# Icons #}
- {% for icon in resource.urls %}
- <span class="icon is-size-4 is-medium" style="margin: 5px;">
- <a href="{{ icon.url }}">
- <i class="{{ icon.icon|as_icon }} is-size-3 resource-icon is-{{ icon.color }}"></i>
- </a>
- </span>
- {% endfor %}
+ <div class="is-flex is-align-items-center">
+ {# Add primary link #}
+ {% if "title_url" in resource %}
+ <span class="icon is-size-4" style="margin: 5px;">
+ <a href="{{ resource.title_url }}">
+ <i class="fas fa-external-link-alt fa-fw is-size-4 resource-icon is-hoverable is-primary"></i>
+ </a>
+ </span>
+ {% endif %}
+
+ {# Add all additional icon #}
+ {% for icon in resource.urls %}
+ <span class="icon is-size-4" style="margin: 5px;">
+ <a href="{{ icon.url }}">
+ <i class="{{ icon.icon|as_icon }} fa-fw is-size-4 resource-icon is-hoverable is-{{ icon.color }}"></i>
+ </a>
+ </span>
+ {% endfor %}
+
+ {# Tags #}
+ <div class="resource-tag-container is-flex ml-auto is-flex-wrap-wrap is-justify-content-end">
+ {% for tag in resource.tags.topics %}
+ <span
+ class="tag resource-tag is-primary is-light ml-2 mt-2"
+ data-filter-name="topics"
+ data-filter-item="{{ tag|to_kebabcase }}"
+ >
+ <i class="{{ tag|title|get_category_icon }} mr-1"></i>
+ {{ tag|title }}
+ </span>
+ {% endfor %}
+ {% for tag in resource.tags.type %}
+ <span
+ class="tag resource-tag has-background-success-light has-text-success-dark ml-2 mt-2"
+ data-filter-name="type"
+ data-filter-item="{{ tag|to_kebabcase }}"
+ >
+ <i class="{{ tag|title|get_category_icon }} mr-1"></i>
+ {{ tag|title }}
+ </span>
+ {% endfor %}
+ {% for tag in resource.tags.payment_tiers %}
+ <span
+ class="tag resource-tag has-background-danger-light has-text-danger-dark ml-2 mt-2"
+ data-filter-name="payment-tiers"
+ data-filter-item="{{ tag|to_kebabcase }}"
+ >
+ <i class="{{ tag|title|get_category_icon }} mr-1"></i>
+ {{ tag|title }}
+ </span>
+ {% endfor %}
+ {% for tag in resource.tags.difficulty %}
+ <span
+ class="tag resource-tag has-background-info-light has-text-info-dark ml-2 mt-2"
+ data-filter-name="difficulty"
+ data-filter-item="{{ tag|to_kebabcase }}"
+ >
+ <i class="{{ tag|title|get_category_icon }} mr-1"></i>
+ {{ tag|title }}
+ </span>
+ {% endfor %}
+ </div>
+ </div>
</div>
diff --git a/pydis_site/templates/resources/resource_box_header.html b/pydis_site/templates/resources/resource_box_header.html
index 84e1a79b..dfbdd92f 100644
--- a/pydis_site/templates/resources/resource_box_header.html
+++ b/pydis_site/templates/resources/resource_box_header.html
@@ -17,8 +17,7 @@
<span class="is-size-4 has-text-weight-bold">
{% if 'title_image' in resource %}
<img src="{{ resource.title_image }}" alt="" style="height: 50px; {{ resource.title_image_style }}">
- {% endif %}
- {% if 'name' in resource %}
+ {% elif 'name' in resource %}
{{ resource.name }}
{% endif %}
</span>
diff --git a/pydis_site/templates/resources/resources.html b/pydis_site/templates/resources/resources.html
index f1f487cf..101f9965 100644
--- a/pydis_site/templates/resources/resources.html
+++ b/pydis_site/templates/resources/resources.html
@@ -1,90 +1,180 @@
{% extends 'base/base.html' %}
+{% load as_icon %}
+{% load to_kebabcase %}
+{% load get_category_icon %}
{% load static %}
{% block title %}Resources{% endblock %}
{% block head %}
- <link rel="stylesheet" href="{% static "css/resources/resources.css" %}">
+ {# Inject a JSON object of all valid filter types from the view #}
+ <script>
+ const validFilters = {{ valid_filters | safe }}
+ </script>
+
+ <link rel="stylesheet" href="{% static "css/resources/resources.css" %}">
+ <link rel="stylesheet" href="{% static "css/collapsibles.css" %}">
+ <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+ <script defer src="{% static "js/resources/resources.js" %}"></script>
+ <script defer src="{% static "js/collapsibles.js" %}"></script>
+ <script defer src="{% static "js/fuzzysort/fuzzysort.js" %}"></script>
{% endblock %}
{% block content %}
- {% include "base/navbar.html" %}
+ {% include "base/navbar.html" %}
+ <input type="hidden" id="resource-type-input" value="{{ resource_type }}">
+ <section class="section">
+ <div class="columns is-variable is-6 is-centered">
+ {# Filtering toolbox #}
+ <div class="column filtering-column is-one-third">
+ <div class="content is-justify-content-center">
+ <nav id="resource-filtering-panel" class="panel is-primary">
+ <p class="panel-heading has-text-centered" id="filter-panel-header">Filter resources</p>
+
+ {# Search bar #}
+ <p id="resource-search" class="control has-icons-left">
+ <input class="input" placeholder="Search resources ">
+ <span class="icon is-small is-left">
+ <i class="fas fa-magnifying-glass"></i>
+ </span>
+ </p>
+
+
+ {# Filter box tags #}
+ <div class="card filter-tags">
+ <div class="is-flex ml-auto">
+ <div id="tag-pool">
+ {# A filter tag for when there are no filters active #}
+ <span class="tag no-tags-selected is-secondary ml-2 mt-2">
+ <i class="fas fa-fw fa-ban mr-1"></i>
+ No filters selected
+ </span>
+
+ {# A filter tag for search queries #}
+ <span class="tag search-query is-secondary ml-2 mt-2">
+ <i class="fas fa-fw fa-magnifying-glass mr-1"></i>
+ <span class="tag inner">Search: ...</span>
+ </span>
+
+ {% for filter_name, filter_data in filters.items %}
+ {% for filter_item in filter_data.filters %}
+ {% if filter_name == "Difficulty" %}
+ <span
+ class="filter-box-tag tag has-background-info-light has-text-info-dark ml-2 mt-2"
+ data-filter-name="{{ filter_name|to_kebabcase }}"
+ data-filter-item="{{ filter_item|to_kebabcase }}"
+ >
+ <i class="{{ filter_item|title|get_category_icon }} mr-1"></i>
+ {{ filter_item|title }}
+ <button class="delete is-small is-info has-background-info-light"></button>
+ </span>
+ {% endif %}
+ {% if filter_name == "Type" %}
+ <span
+ class="filter-box-tag tag has-background-success-light has-text-success-dark ml-2 mt-2"
+ data-filter-name="{{ filter_name|to_kebabcase }}"
+ data-filter-item="{{ filter_item|to_kebabcase }}"
+ >
+ <i class="{{ filter_item|title|get_category_icon }} mr-1"></i>
+ {{ filter_item|title }}
+ <button class="delete is-small is-success has-background-success-light"></button>
+ </span>
+ {% endif %}
+ {% if filter_name == "Payment tiers" %}
+ <span
+ class="filter-box-tag tag has-background-danger-light has-text-danger-dark ml-2 mt-2"
+ data-filter-name="{{ filter_name|to_kebabcase }}"
+ data-filter-item="{{ filter_item|to_kebabcase }}"
+ >
+ <i class="{{ filter_item|title|get_category_icon }} mr-1"></i>
+ {{ filter_item|title }}
+ <button class="delete is-small is-danger has-background-danger-light"></button>
+ </span>
+ {% endif %}
+ {% if filter_name == "Topics" %}
+ <span
+ class="filter-box-tag tag is-primary is-light ml-2 mt-2"
+ data-filter-name="{{ filter_name|to_kebabcase }}"
+ data-filter-item="{{ filter_item|to_kebabcase }}"
+ >
+ <i class="{{ filter_item|title|get_category_icon }} mr-1"></i>
+ {{ filter_item|title }}
+ <button class="delete is-small is-primary has-background-primary-light"></button>
+ </span>
+ {% endif %}
+ {% endfor %}
+ {% endfor %}
+ </div>
+ <div class="close-filters-button">
+ {# A little x in the top right, visible only when filters are active, which removes all filters. #}
+ <a class="icon">
+ <i class="fas fa-window-close"></i>
+ </a>
- <section class="section">
- <div class="container">
- <div class="content">
- <h1>Resources</h1>
+ </div>
+ </div>
+ </div>
- <div class="tile is-ancestor">
- <a class="tile is-parent" href="{% url "content:page_category" location="guides" %}">
- <article class="tile is-child box hero is-primary is-bold">
- <p class="title is-size-1"><i class="fad fa-info-circle" aria-hidden="true"></i> Guides</p>
- <p class="subtitle is-size-4">Made by us, for you</p>
- </article>
- </a>
+ {# Filter checkboxes #}
+ {% for filter_name, filter_data in filters.items %}
+ <div class="card filter-category-header">
+ <button type="button" class="card-header collapsible">
+ <span class="card-header-title subtitle is-6 my-2 ml-2">
+ <i class="fa-fw {{ filter_data.icon }} is-primary" aria-hidden="true"></i>&nbsp&nbsp{{ filter_name }}
+ </span>
+ <span class="card-header-icon">
+ {% if not filter_data.hidden %}
+ <i class="far fa-fw fa-window-minimize is-6 title" aria-hidden="true"></i>
+ {% else %}
+ <i class="fas fa-fw fa-angle-down is-6 title" aria-hidden="true"></i>
+ {% endif %}
+ </span>
+ </button>
- <div class="tile is-vertical is-9">
- <div class="tile">
- <a class="tile is-8 is-parent" href="{% url "resources:resources" category="reading" %}">
- <article class="tile is-child box hero is-black" id="readingBlock">
- <p class="title is-size-1"><i class="fad fa-book-alt" aria-hidden="true"></i> Read</p>
- <p class="subtitle is-size-4">Lovingly curated books to explore</p>
- </article>
- </a>
+ {# Checkboxes #}
+ {% if filter_data.hidden %}
+ <div class="collapsible-content collapsed">
+ {% else %}
+ <div class="collapsible-content">
+ {% endif %}
+ <div class="card-content">
+ {% for filter_item in filter_data.filters %}
+ <a class="panel-block filter-panel">
+ <label class="checkbox">
+ <input
+ class="filter-checkbox"
+ type="checkbox"
+ data-filter-name="{{ filter_name|to_kebabcase }}"
+ data-filter-item="{{ filter_item|to_kebabcase }}"
+ >
+ {{ filter_item }}
+ </label>
+ </a>
+ {% endfor %}
+ </div>
+ </div>
+ </div>
+ {% endfor %}
+ </nav>
+ </div>
+ </div>
- <div class="tile">
- <a class="tile is-parent" href="{% url "resources:resources" category="videos" %}">
- <article class="tile is-child box hero is-danger is-bold">
- <p class="title is-size-1"><i class="fad fa-video" aria-hidden="true"></i> Watch</p>
- <p class="subtitle is-size-4">Visually engaging</p>
- </article>
- </a>
- </div>
- </div>
+ <div class="column is-two-thirds">
+ {# Message to display when there are no hits #}
+ <div class="no-resources-found">
+ <h2 class="title is-3 has-text-centered pt-0 pb-6">No matching resources found!</h2>
+ <img src="{% static "images/resources/duck_pond_404.jpg" %}">
+ </div>
- <div class="tile">
- <a class="tile is-parent" href="{% url "resources:resources" category="interactive" %}">
- <article class="tile is-child box hero is-black" id="interactiveBlock">
- <p class="title is-size-1"><i class="fad fa-code" aria-hidden="true"></i> Try</p>
- <p class="subtitle is-size-4">Interactively discover the possibilities</p>
- </article>
- </a>
- <a class="tile is-8 is-parent" href="{% url "resources:resources" category="courses" %}">
- <article class="tile is-child box hero is-success is-bold">
- <p class="title is-size-1"><i class="fad fa-graduation-cap" aria-hidden="true"></i> Learn</p>
- <p class="subtitle is-size-4">Structured courses with clear goals</p>
- </article>
- </a>
- </div>
- </div>
- </div>
- <div class="tile is-ancestor">
- <div class="tile is-vertical is-9">
- <div class="tile">
- <a class="tile is-8 is-parent" href="{% url "resources:resources" category="communities" %}">
- <article class="tile is-child box hero is-black" id="communitiesBlock">
- <p class="title is-size-1"><i class="fad fa-users" aria-hidden="true"></i> Communities</p>
- <p class="subtitle is-size-4">Some of our best friends</p>
- </article>
- </a>
- <div class="tile">
- <a class="tile is-parent" href="{% url "resources:resources" category="podcasts" %}">
- <article class="tile is-child box hero is-black" id="podcastsBlock">
- <p class="title is-size-1"><i class="fad fa-podcast" aria-hidden="true"></i> Listen</p>
- <p class="subtitle is-size-4">Regular podcasts to follow</p>
- </article>
- </a>
- </div>
- </div>
- </div>
- <a class="tile is-parent" href="{% url "resources:resources" category="tools" %}">
- <article class="tile is-child box hero is-dark">
- <p class="title is-size-1"><i class="fad fa-tools" aria-hidden="true"></i> Tools</p>
- <p class="subtitle is-size-4">Things we love to use</p>
- </article>
- </a>
- </div>
- </div>
- </div>
- </section>
+ {# Resource cards #}
+ <div class="content is-flex is-justify-content-center">
+ <div class="container is-fullwidth">
+ {% for resource in resources.values %}
+ {% include "resources/resource_box.html" %}
+ {% endfor %}
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
{% endblock %}
diff --git a/pydis_site/templates/resources/resources_list.html b/pydis_site/templates/resources/resources_list.html
deleted file mode 100644
index e2be3cb7..00000000
--- a/pydis_site/templates/resources/resources_list.html
+++ /dev/null
@@ -1,52 +0,0 @@
-{% extends "base/base.html" %}
-{% load as_icon %}
-{% load static %}
-
-{% block title %}{{ category_info.name }}{% endblock %}
-{% block head %}
- <link rel="stylesheet" href="{% static "css/resources/resources_list.css" %}">
-{% endblock %}
-
-{% block content %}
- {% include "base/navbar.html" %}
-
- <section class="section breadcrumb-section">
- <div class="container">
- <nav class="breadcrumb is-pulled-left" aria-label="breadcrumbs">
- <ul>
- <li><a href="{% url "resources:index" %}">Resources</a></li>
- <li class="is-active"><a href="#">{{ category_info.name }}</a></li>
- </ul>
- </nav>
- </div>
- </section>
-
- <section class="section">
- <div class="container">
- <div class="content">
- <h1>{{ category_info.name }}</h1>
- <p>{{ category_info.description|safe }}</p>
- <div>
- {% for resource in resources|dictsort:"position" %}
- {% include "resources/resource_box.html" %}
- {% endfor %}
-
- {% for subcategory in subcategories|dictsort:"category_info.position" %}
- <h2 id="{{ subcategory.category_info.raw_name }}">
- <a href="{% url "resources:resources" category=category_info.raw_name %}#{{ subcategory.category_info.raw_name }}">
- {{ subcategory.category_info.name }}
- </a>
- </h2>
- <p>{{ subcategory.category_info.description|safe }}</p>
-
- {% for resource in subcategory.resources|dictsort:"position" %}
- {% with category_info=subcategory.category_info %}
- {% include "resources/resource_box.html" %}
- {% endwith %}
- {% endfor %}
- {% endfor %}
- </div>
- </div>
- </div>
- </section>
-{% endblock %}
diff --git a/pydis_site/urls.py b/pydis_site/urls.py
index 51ef4214..6cd31f26 100644
--- a/pydis_site/urls.py
+++ b/pydis_site/urls.py
@@ -11,19 +11,25 @@ NON_STATIC_PATTERNS = [
# Internal API ingress (cluster local)
path('pydis-api/', include('pydis_site.apps.api.urls', namespace='internal_api')),
- # This must be mounted before the `content` app to prevent Django
- # from wildcard matching all requests to `pages/...`.
- path('', include('pydis_site.apps.redirect.urls')),
path('', include('django_prometheus.urls')),
-
- path('staff/', include('pydis_site.apps.staff.urls', namespace='staff')),
] if not settings.env("STATIC_BUILD") else []
urlpatterns = (
*NON_STATIC_PATTERNS,
+
+ # This must be mounted before the `content` app to prevent Django
+ # from wildcard matching all requests to `pages/...`.
+ path('', include('pydis_site.apps.redirect.urls')),
+
path('pages/', include('pydis_site.apps.content.urls', namespace='content')),
path('resources/', include('pydis_site.apps.resources.urls')),
path('events/', include('pydis_site.apps.events.urls', namespace='events')),
path('', include('pydis_site.apps.home.urls', namespace='home')),
)
+
+
+if not settings.env("STATIC_BUILD"):
+ urlpatterns += (
+ path('staff/', include('pydis_site.apps.staff.urls', namespace='staff')),
+ )
diff --git a/pyproject.toml b/pyproject.toml
index 2f1322e3..b350836e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,12 +7,12 @@ license = "MIT"
[tool.poetry.dependencies]
python = "3.9.*"
-django = "~=3.0.4"
+django = "~=3.1.14"
django-environ = "~=0.4.5"
-django-filter = "~=2.1.0"
-djangorestframework = "~=3.11.0"
+django-filter = "~=21.1"
+djangorestframework = "~=3.12.0"
psycopg2-binary = "~=2.8.0"
-django-simple-bulma = "~=2.1"
+django-simple-bulma = "~=2.4"
whitenoise = "~=5.0"
requests = "~=2.21"
pyyaml = "~=5.1"
@@ -37,7 +37,7 @@ flake8-todo = "~=0.7"
mccabe = "~=0.6.1"
pep8-naming = "~=0.9"
pre-commit = "~=2.1"
-pyfakefs = "~=4.4.0"
+pyfakefs = "~=4.5"
coveralls = "~=2.1"
taskipy = "~=1.7.0"
python-dotenv = "~=0.17.1"
@@ -51,7 +51,8 @@ start = "python manage.py run --debug"
makemigrations = "python manage.py makemigrations"
django_shell = "python manage.py shell"
test = "coverage run manage.py test"
+coverage = "coverage run manage.py test --no-input; coverage report -m"
report = "coverage report -m"
lint = "pre-commit run --all-files"
precommit = "pre-commit install"
-static = "python mange.py distill-local build --traceback --force"
+static = "python manage.py distill-local build --traceback --force"
diff --git a/static-builds/README.md b/static-builds/README.md
index b5cba896..9b86ed08 100644
--- a/static-builds/README.md
+++ b/static-builds/README.md
@@ -17,19 +17,15 @@ They are split into two parts:
To get started with building, you can use the following command:
```shell
+poetry install
python -m pip install httpx==0.19.0
-python manage.py distill-local build --traceback --force --collectstatic
+poetry run task static
```
Alternatively, you can use the [Dockerfile](/Dockerfile) and extract the build.
Both output their builds to a `build/` directory.
-> Warning: If you are modifying the [build script](./netlify_build.py), make sure it is compatible with Python 3.8.
-
-Note: The build script uses [nightly.link](https://github.com/oprypin/nightly.link)
-to fetch the artifact with no verification.
-
### Deploying To Netlify
To deploy to netlify, link your site GitHub repository to a netlify site, and use the following settings:
@@ -46,3 +42,9 @@ Environment Variables:
Note that at this time, if you are deploying to netlify yourself, you won't have access to the
fa-icons pack we are using, which will lead to many missing icons on your preview.
You can either update the pack to one which will work on your domain, or you'll have to live with the missing icons.
+
+
+> Warning: If you are modifying the [build script](./netlify_build.py), make sure it is compatible with Python 3.8.
+
+Note: The build script uses [nightly.link](https://github.com/oprypin/nightly.link)
+to fetch the artifact with no authentication.