diff options
Diffstat (limited to 'pydis_site')
15 files changed, 166 insertions, 12 deletions
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. |