aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--poetry.lock51
-rw-r--r--pydis_site/apps/api/tests/test_bumped_threads.py5
-rw-r--r--pydis_site/apps/api/tests/test_filters.py9
-rw-r--r--pydis_site/apps/api/tests/test_infractions.py47
-rw-r--r--pydis_site/apps/api/tests/test_nominations.py4
-rw-r--r--pydis_site/apps/api/tests/test_roles.py5
-rw-r--r--pydis_site/apps/api/tests/test_users.py2
-rw-r--r--pydis_site/apps/api/urls.py6
-rw-r--r--pydis_site/apps/api/viewsets/bot/infraction.py13
-rw-r--r--pydis_site/apps/api/viewsets/bot/user.py9
-rw-r--r--pydis_site/apps/resources/resources/missing-semester.yaml15
-rw-r--r--pydis_site/apps/timeline/entries/2023-10-15_pydis-code-jam-2023-cj10.md19
-rw-r--r--pydis_site/apps/timeline/entries/2024-06-20_partnership-with-coveragepy.md8
-rw-r--r--pydis_site/apps/timeline/entries/2024-09-01_pydis-code-jam-2024-cj11.md12
-rw-r--r--pydis_site/apps/timeline/entries/2024-10-07_python-313-release-stream.md16
-rw-r--r--pydis_site/apps/timeline/entries/2025-04-10_400000-members.md8
-rw-r--r--pyproject.toml4
17 files changed, 187 insertions, 46 deletions
diff --git a/poetry.lock b/poetry.lock
index 3f425adb..a6d0f0a7 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -507,19 +507,18 @@ dev = ["PyGithub (>=1.43,<2.0)", "flake8 (>=3.8,<4.0)", "flake8-annotations (>=2
[[package]]
name = "djangorestframework"
-version = "3.14.0"
+version = "3.16.0"
description = "Web APIs for Django, made easy."
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.9"
groups = ["main"]
files = [
- {file = "djangorestframework-3.14.0-py3-none-any.whl", hash = "sha256:eb63f58c9f218e1a7d064d17a70751f528ed4e1d35547fdade9aaf4cd103fd08"},
- {file = "djangorestframework-3.14.0.tar.gz", hash = "sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8"},
+ {file = "djangorestframework-3.16.0-py3-none-any.whl", hash = "sha256:bea7e9f6b96a8584c5224bfb2e4348dfb3f8b5e34edbecb98da258e892089361"},
+ {file = "djangorestframework-3.16.0.tar.gz", hash = "sha256:f022ff46613584de994c0c6a4aebbace5fd700555fbe9d33b865ebf173eba6c9"},
]
[package.dependencies]
-django = ">=3.0"
-pytz = "*"
+django = ">=4.2"
[[package]]
name = "filelock"
@@ -562,31 +561,31 @@ tornado = ["tornado (>=0.2)"]
[[package]]
name = "h11"
-version = "0.14.0"
+version = "0.16.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
groups = ["main"]
files = [
- {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
- {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
+ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"},
+ {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"},
]
[[package]]
name = "httpcore"
-version = "1.0.7"
+version = "1.0.9"
description = "A minimal low-level HTTP client."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
- {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"},
- {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"},
+ {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"},
+ {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"},
]
[package.dependencies]
certifi = "*"
-h11 = ">=0.13,<0.15"
+h11 = ">=0.16"
[package.extras]
asyncio = ["anyio (>=4.0,<5.0)"]
@@ -667,18 +666,18 @@ files = [
[[package]]
name = "markdown"
-version = "3.7"
+version = "3.8"
description = "Python implementation of John Gruber's Markdown."
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
groups = ["main"]
files = [
- {file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"},
- {file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"},
+ {file = "markdown-3.8-py3-none-any.whl", hash = "sha256:794a929b79c5af141ef5ab0f2f642d0f7b1872981250230e72682346f7cc90dc"},
+ {file = "markdown-3.8.tar.gz", hash = "sha256:7df81e63f0df5c4b24b7d156eb81e4690595239b7d70937d0409f1b0de319c6f"},
]
[package.extras]
-docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"]
+docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"]
testing = ["coverage", "pyyaml"]
[[package]]
@@ -987,18 +986,6 @@ docs = ["sphinx"]
test = ["mypy", "pyaml", "pytest", "toml", "types-PyYAML", "types-toml"]
[[package]]
-name = "pytz"
-version = "2025.1"
-description = "World timezone definitions, modern and historical"
-optional = false
-python-versions = "*"
-groups = ["main"]
-files = [
- {file = "pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57"},
- {file = "pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e"},
-]
-
-[[package]]
name = "pyyaml"
version = "6.0.2"
description = "YAML parser and emitter for Python"
@@ -1338,4 +1325,4 @@ brotli = ["brotli"]
[metadata]
lock-version = "2.1"
python-versions = "3.11.*"
-content-hash = "afb37c0f864029a42421300acc56d258c26e8449c2067a17a7ecc8401b9bcea1"
+content-hash = "56a98384ebe7576c4c0afabb800a758bbef4a36040a259b476f89055a4d058c9"
diff --git a/pydis_site/apps/api/tests/test_bumped_threads.py b/pydis_site/apps/api/tests/test_bumped_threads.py
index 2e3892c7..fbd21e92 100644
--- a/pydis_site/apps/api/tests/test_bumped_threads.py
+++ b/pydis_site/apps/api/tests/test_bumped_threads.py
@@ -60,4 +60,7 @@ class BumpedThreadAPITests(AuthenticatedAPITestCase):
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
- self.assertEqual(response.json(), {"detail": "Not found."})
+ self.assertEqual(
+ response.json(),
+ {"detail": "No BumpedThread matches the given query."},
+ )
diff --git a/pydis_site/apps/api/tests/test_filters.py b/pydis_site/apps/api/tests/test_filters.py
index 96b3a65c..831c4649 100644
--- a/pydis_site/apps/api/tests/test_filters.py
+++ b/pydis_site/apps/api/tests/test_filters.py
@@ -210,8 +210,15 @@ class GenericFilterTests(AuthenticatedAPITestCase):
sequence.model.objects.all().delete()
response = self.client.get(f"{sequence.url()}/42")
+ if "filter-lists" in sequence.url():
+ kind = "FilterList"
+ else:
+ kind = "Filter"
self.assertEqual(response.status_code, 404)
- self.assertDictEqual(response.json(), {'detail': 'Not found.'})
+ self.assertDictEqual(
+ response.json(),
+ {'detail': f"No {kind} matches the given query."},
+ )
def test_creation(self) -> None:
for name, sequence in get_test_sequences().items():
diff --git a/pydis_site/apps/api/tests/test_infractions.py b/pydis_site/apps/api/tests/test_infractions.py
index 82722285..ada2cd33 100644
--- a/pydis_site/apps/api/tests/test_infractions.py
+++ b/pydis_site/apps/api/tests/test_infractions.py
@@ -327,6 +327,18 @@ class InfractionTests(AuthenticatedAPITestCase):
self.assertEqual(infraction.type, self.ban_hidden.type)
self.assertEqual(infraction.hidden, self.ban_hidden.hidden)
+ def test_partial_update_of_inactive_infraction(self):
+ url = reverse('api:bot:infraction-detail', args=(self.ban_hidden.id,))
+ data = {
+ 'expires_at': '4143-02-15T21:04:31+00:00',
+ 'reason': "Do not help out Joe with any electricity-related questions"
+ }
+ self.assertFalse(self.ban_inactive.active, "Infraction should start out as inactive")
+ response = self.client.patch(url, data=data)
+ self.assertEqual(response.status_code, 200)
+ infraction = Infraction.objects.get(id=self.ban_inactive.id)
+ self.assertFalse(infraction.active, "Infraction changed to active via patch")
+
def test_partial_update_returns_400_for_frozen_field(self):
url = reverse('api:bot:infraction-detail', args=(self.ban_hidden.id,))
data = {'user': 6}
@@ -559,7 +571,7 @@ class CreationTests(AuthenticatedAPITestCase):
second_response.json(),
{
'non_field_errors': [
- 'This user already has an active infraction of this type.'
+ 'The fields user, type must make a unique set.'
]
}
)
@@ -692,6 +704,37 @@ class CreationTests(AuthenticatedAPITestCase):
reason='A reason.',
)
+ def test_accepts_two_warnings(self):
+ """Two warnings can be created for a user."""
+ url = reverse('api:bot:infraction-list')
+ data = {
+ 'user': self.user.id,
+ 'actor': self.user.id,
+ 'type': 'warning',
+ 'reason': "Thought upgrading DRF is a smart idea",
+ 'active': False,
+ }
+ first_response = self.client.post(url, data=data)
+ self.assertEqual(first_response.status_code, 201)
+ data['reason'] = "Do not claim King Arthur eats children"
+ second_response = self.client.post(url, data=data)
+ self.assertEqual(second_response.status_code, 201)
+
+ def test_respects_active_false_of_warnings(self):
+ """Infractions can be created as inactive."""
+ url = reverse('api:bot:infraction-list')
+ data = {
+ 'user': self.user.id,
+ 'actor': self.user.id,
+ 'type': 'warning',
+ 'reason': "Associate of REDACTED",
+ 'active': False,
+ }
+ response = self.client.post(url, data=data)
+ self.assertEqual(response.status_code, 201)
+ infraction = Infraction.objects.get(id=response.json()['id'])
+ self.assertFalse(infraction.active)
+
class InfractionDeletionTests(AuthenticatedAPITestCase):
@classmethod
@@ -824,7 +867,7 @@ class SerializerTests(AuthenticatedAPITestCase):
self.create_infraction('ban', active=True)
instance = self.create_infraction('ban', active=False)
- data = {'reason': 'hello'}
+ data = {'reason': 'hello', 'active': True}
serializer = InfractionSerializer(instance, data=data, partial=True)
self.assertTrue(serializer.is_valid(), msg=serializer.errors)
diff --git a/pydis_site/apps/api/tests/test_nominations.py b/pydis_site/apps/api/tests/test_nominations.py
index e4dfe36a..7c6f1bbb 100644
--- a/pydis_site/apps/api/tests/test_nominations.py
+++ b/pydis_site/apps/api/tests/test_nominations.py
@@ -379,7 +379,7 @@ class NominationTests(AuthenticatedAPITestCase):
response = self.client.get(url, data={})
self.assertEqual(response.status_code, 404)
self.assertEqual(response.json(), {
- "detail": "Not found."
+ "detail": "No Nomination matches the given query."
})
def test_returns_404_on_patch_unknown_nomination(self):
@@ -391,7 +391,7 @@ class NominationTests(AuthenticatedAPITestCase):
response = self.client.patch(url, data={})
self.assertEqual(response.status_code, 404)
self.assertEqual(response.json(), {
- "detail": "Not found."
+ "detail": "No Nomination matches the given query."
})
def test_returns_405_on_list_put(self):
diff --git a/pydis_site/apps/api/tests/test_roles.py b/pydis_site/apps/api/tests/test_roles.py
index d3031990..da879b00 100644
--- a/pydis_site/apps/api/tests/test_roles.py
+++ b/pydis_site/apps/api/tests/test_roles.py
@@ -208,4 +208,7 @@ class CreationTests(AuthenticatedAPITestCase):
for method in ('get', 'put', 'patch', 'delete'):
response = getattr(self.client, method)(url)
self.assertEqual(response.status_code, 404)
- self.assertJSONEqual(response.content, '{"detail": "Not found."}')
+ self.assertJSONEqual(
+ response.content,
+ '{"detail": "No Role matches the given query."}',
+ )
diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py
index 3799b631..99673e53 100644
--- a/pydis_site/apps/api/tests/test_users.py
+++ b/pydis_site/apps/api/tests/test_users.py
@@ -746,7 +746,7 @@ class UserAltUpdateTests(AuthenticatedAPITestCase):
response = self.client.post(repeated_url, repeated_data)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json(), {
- 'source': ["This relationship has already been established"]
+ 'non_field_errors': ["The fields source, target must make a unique set."]
})
def test_removing_existing_alt_source_from_target(self) -> None:
diff --git a/pydis_site/apps/api/urls.py b/pydis_site/apps/api/urls.py
index 5cda033a..ae3172c7 100644
--- a/pydis_site/apps/api/urls.py
+++ b/pydis_site/apps/api/urls.py
@@ -28,9 +28,11 @@ from .viewsets import (
# https://www.django-rest-framework.org/api-guide/routers/#defaultrouter
bot_router = DefaultRouter(trailing_slash=False)
+# XXX: We should probably figure out why we have this registered twice.
bot_router.register(
'filter/filter_lists',
- FilterListViewSet
+ FilterListViewSet,
+ basename="filter-filterlists-list",
)
bot_router.register(
"aoc-account-links",
@@ -62,7 +64,7 @@ bot_router.register(
)
bot_router.register(
'filter-lists',
- FilterListViewSet
+ FilterListViewSet,
)
bot_router.register(
'infractions',
diff --git a/pydis_site/apps/api/viewsets/bot/infraction.py b/pydis_site/apps/api/viewsets/bot/infraction.py
index 8da82822..1d16232c 100644
--- a/pydis_site/apps/api/viewsets/bot/infraction.py
+++ b/pydis_site/apps/api/viewsets/bot/infraction.py
@@ -161,6 +161,12 @@ class InfractionViewSet(
def partial_update(self, request: HttpRequest, *_args, **_kwargs) -> Response:
"""Method that handles the nuts and bolts of updating an Infraction."""
instance = self.get_object()
+ # DRF presently errors out if we are not specifying all fields here.
+ # See this issue:
+ # https://github.com/encode/django-rest-framework/issues/9358. The
+ # merged PR that closed the issue does not appear to work either, so
+ # here's a workaround.
+ request.data.setdefault("active", instance.active)
serializer = self.get_serializer(instance, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
@@ -284,7 +290,12 @@ class InfractionViewSet(
"""
try:
return super().create(request, *args, **kwargs)
- except IntegrityError as err:
+ except IntegrityError as err: # pragma: no cover - see below
+ # Not covered: DRF handles this via `UniqueTogetherValidator` these
+ # days, which means it's hard to test this branch specifically.
+ # However, in a productive deployment, it's still very much
+ # possible for two concurrent inserts to run into IntegrityError.
+
# We need to use `__cause__` here, as Django reraises the internal
# UniqueViolation emitted by psycopg2 (which contains the attribute
# that we actually need)
diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py
index c0b4ca0f..79e867c3 100644
--- a/pydis_site/apps/api/viewsets/bot/user.py
+++ b/pydis_site/apps/api/viewsets/bot/user.py
@@ -395,7 +395,14 @@ class UserViewSet(ModelViewSet):
raise ParseError(detail={
"source": ["The user may not be an alternate account of itself"]
})
- if err.__cause__.diag.constraint_name == 'api_useraltrelationship_unique_relationships':
+ if (
+ err.__cause__.diag.constraint_name == 'api_useraltrelationship_unique_relationships'
+ ): # pragma: no cover - see below
+ # This is not covered because newer DRF versions automatically validate this,
+ # however the validation is done via a SELECT query which may race concurrent
+ # inserts in prod. The only correct way is e.g. what Ecto does which is
+ # associating the validators to the unique constraint errors to match in
+ # errors, anything else may race.
raise ParseError(detail={
"source": ["This relationship has already been established"]
})
diff --git a/pydis_site/apps/resources/resources/missing-semester.yaml b/pydis_site/apps/resources/resources/missing-semester.yaml
new file mode 100644
index 00000000..b05d174c
--- /dev/null
+++ b/pydis_site/apps/resources/resources/missing-semester.yaml
@@ -0,0 +1,15 @@
+description: This course covers the fundamental but often overlooked skills that programmers use everyday,
+ such as using the shell, the command-line environment, and using the modal text editors like vim.
+ The course structure allows you to focus on the topics you want to and skip ones you don't think are relevant.
+name: The Missing Semester of Your CS Education
+title_url: https://missing.csail.mit.edu/
+tags:
+ topics:
+ - general
+ - other
+ payment_tiers:
+ - free
+ difficulty:
+ - beginner
+ type:
+ - course
diff --git a/pydis_site/apps/timeline/entries/2023-10-15_pydis-code-jam-2023-cj10.md b/pydis_site/apps/timeline/entries/2023-10-15_pydis-code-jam-2023-cj10.md
new file mode 100644
index 00000000..34887f69
--- /dev/null
+++ b/pydis_site/apps/timeline/entries/2023-10-15_pydis-code-jam-2023-cj10.md
@@ -0,0 +1,19 @@
+---
+title: Summer Code Jam 2023 (CJ10)
+date: October 15th, 2023
+icon: fa fa-dice
+icon_color: pastel-lime
+---
+
+We host the 10th Code Jam. This year, teams had to use an **image
+processing/manipulation library** to create something under the theme of
+**"Secret Codes"**.
+
+In total, 17 teams submitted projects out of which we showcased the top 10 in a
+livestream, where the winners were also announced.
+
+<div class="force-aspect-container">
+<iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
+allowfullscreen="" class="force-aspect-content" frameborder="0"
+src="https://www.youtube.com/embed/7U7wu7o1Dss"></iframe>
+</div>
diff --git a/pydis_site/apps/timeline/entries/2024-06-20_partnership-with-coveragepy.md b/pydis_site/apps/timeline/entries/2024-06-20_partnership-with-coveragepy.md
new file mode 100644
index 00000000..19582afe
--- /dev/null
+++ b/pydis_site/apps/timeline/entries/2024-06-20_partnership-with-coveragepy.md
@@ -0,0 +1,8 @@
+---
+title: Partnership with Coverage.py
+date: June 20th, 2024
+icon: fa fa-handshake
+
+---
+
+The `#coveragepy` channel is created for discussion of Coverage.py, the popular code coverage measurement tool.
diff --git a/pydis_site/apps/timeline/entries/2024-09-01_pydis-code-jam-2024-cj11.md b/pydis_site/apps/timeline/entries/2024-09-01_pydis-code-jam-2024-cj11.md
new file mode 100644
index 00000000..cb944e71
--- /dev/null
+++ b/pydis_site/apps/timeline/entries/2024-09-01_pydis-code-jam-2024-cj11.md
@@ -0,0 +1,12 @@
+---
+title: Code Jam 2024 (CJ11)
+date: September 1st, 2024
+icon: fa fa-dice
+icon_color: pastel-lime
+---
+
+We host the 11th Code Jam. This year, teams had to create a **Discord
+Application** based on the theme of **"Information Overload"**.
+
+23 teams submitted projects, 10 of which were demoed in a livestream in which
+we also announced the winners.
diff --git a/pydis_site/apps/timeline/entries/2024-10-07_python-313-release-stream.md b/pydis_site/apps/timeline/entries/2024-10-07_python-313-release-stream.md
new file mode 100644
index 00000000..d1fc3eca
--- /dev/null
+++ b/pydis_site/apps/timeline/entries/2024-10-07_python-313-release-stream.md
@@ -0,0 +1,16 @@
+---
+title: Python 3.13 Release Stream
+date: Oct 10th, 2022
+icon: pydis
+---
+
+Another year, another Python release stream! Hosted by KeithTheEE, and CPython
+Core Team members Ɓukasz Langa, Pablo Galindo Salgado, and Brandt Bucher.
+Together, they discuss the new features of the release, and what goes into
+contributing to Python.
+
+<div class="force-aspect-container">
+<iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
+allowfullscreen="" class="force-aspect-content" frameborder="0"
+src="https://www.youtube.com/embed/7MAPzvv3ZG0"></iframe>
+</div>
diff --git a/pydis_site/apps/timeline/entries/2025-04-10_400000-members.md b/pydis_site/apps/timeline/entries/2025-04-10_400000-members.md
new file mode 100644
index 00000000..205f61c7
--- /dev/null
+++ b/pydis_site/apps/timeline/entries/2025-04-10_400000-members.md
@@ -0,0 +1,8 @@
+---
+title: We hit 400 000 members!
+date: April 10th, 2025
+icon: fa fa-users
+icon_color: pastel-dark-blue
+---
+
+We're growing slower, but we're still growing! Python Discord's now has 400,000 members.
diff --git a/pyproject.toml b/pyproject.toml
index b311ba85..f8ba4e28 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -15,10 +15,10 @@ django-environ = "0.11.2"
django-filter = "25.1"
django-prometheus = "2.3.1"
django-simple-bulma = "2.6.0"
-djangorestframework = "3.14.0"
+djangorestframework = "3.16.0"
gunicorn = "23.0.0"
httpx = "0.28.1"
-markdown = "3.7"
+markdown = "3.8"
psycopg = {extras = ["binary"], version = "3.2.6"}
pyjwt = {version = "2.10.1", extras = ["crypto"]}
pymdown-extensions = "10.14.3"