From d061ab1c138c7cecf8d138226990bdcf0761a5db Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Wed, 19 May 2021 13:13:57 +0530 Subject: Add active field to OffTopicChannelName model. --- .../apps/api/migrations/0070_auto_20210519_0545.py | 23 ++++++++++++++++++++++ .../apps/api/models/bot/off_topic_channel_name.py | 7 ++++++- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 pydis_site/apps/api/migrations/0070_auto_20210519_0545.py 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/models/bot/off_topic_channel_name.py b/pydis_site/apps/api/models/bot/off_topic_channel_name.py index 403c7465..582c069e 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): -- cgit v1.2.3 From 8232b1115bdc308c7ca12f477c704957ec3e3ed1 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Wed, 19 May 2021 13:15:53 +0530 Subject: Serialize name and active attribute. --- pydis_site/apps/api/serializers.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index f47bedca..8f61073e 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -207,18 +207,7 @@ class OffTopicChannelNameSerializer(ModelSerializer): """Metadata defined for the Django REST Framework.""" 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', 'active') class ReminderSerializer(ModelSerializer): -- cgit v1.2.3 From b2819a4cfe04b893d5cd638407e48ff6bd501a20 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Wed, 19 May 2021 13:16:21 +0530 Subject: Use ModelViewSet to add support for PUT and PATCH request. --- pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) 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 826ad25e..194e96d2 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 @@ -3,16 +3,15 @@ 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.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. @@ -73,7 +72,7 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): """Returns a queryset that covers the entire OffTopicChannelName table.""" return OffTopicChannelName.objects.all() - def create(self, request: HttpRequest) -> Response: + def create(self, request: HttpRequest, *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: HttpRequest, *args, **kwargs) -> Response: """ DRF method for listing OffTopicChannelName entries. @@ -133,6 +132,4 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): serialized = self.serializer_class(queryset, many=True) return Response(serialized.data) - queryset = self.get_queryset() - serialized = self.serializer_class(queryset, many=True) - return Response(serialized.data) + return super().list(self, request) -- cgit v1.2.3 From 28de14b671cf5c80b08044806b154e6272d86d2c Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Mon, 31 May 2021 15:27:54 +0530 Subject: Return a list of ot-names only for GET request only to bot/off-topic-channel-names --- pydis_site/apps/api/serializers.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 8f61073e..e09c383e 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -200,14 +200,29 @@ class ExpandedInfractionSerializer(InfractionSerializer): return ret +class OffTopicChannelNameListSerializer(ListSerializer): + def update(self, instance, validated_data): + pass + + 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 + + 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', 'active') + fields = ('name', 'used', 'active') class ReminderSerializer(ModelSerializer): -- cgit v1.2.3 From 69f4dc119af4a8d46295abc2551bad24bee96066 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Mon, 31 May 2021 15:27:54 +0530 Subject: Return a list of ot-names only for GET request only to bot/off-topic-channel-names --- pydis_site/apps/api/serializers.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 8f61073e..1acb4fb8 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -1,4 +1,6 @@ """Converters from Django models to data interchange formats and back.""" +from typing import List + from django.db.models.query import QuerySet from django.db.utils import IntegrityError from rest_framework.exceptions import NotFound @@ -200,14 +202,29 @@ class ExpandedInfractionSerializer(InfractionSerializer): return ret +class OffTopicChannelNameListSerializer(ListSerializer): + def update(self, instance, validated_data): + pass + + def to_representation(self, objects: List[OffTopicChannelName]) -> List[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 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', 'active') + fields = ('name', 'used', 'active') class ReminderSerializer(ModelSerializer): -- cgit v1.2.3 From 87e25f77cbc900024e86d439abd9fde71181800d Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Fri, 4 Jun 2021 21:46:54 +0530 Subject: Query off topic names based on Active attribute. --- .../apps/api/viewsets/bot/off_topic_channel_name.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) 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 194e96d2..0caf8088 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,9 +1,11 @@ +import json + 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.response import Response +from rest_framework.request import Request from rest_framework.status import HTTP_201_CREATED from rest_framework.viewsets import ModelViewSet @@ -68,11 +70,11 @@ class OffTopicChannelNameViewSet(ModelViewSet): name = self.kwargs[self.lookup_field] return get_object_or_404(queryset, name=name) - def get_queryset(self) -> QuerySet: + def get_queryset(self, active=True) -> QuerySet: """Returns a queryset that covers the entire OffTopicChannelName table.""" - return OffTopicChannelName.objects.all() + return OffTopicChannelName.objects.filter(active=True) - def create(self, request: HttpRequest, *args, **kwargs) -> Response: + def create(self, request: Request, *args, **kwargs) -> Response: """ DRF method for creating a new OffTopicChannelName. @@ -90,7 +92,7 @@ class OffTopicChannelNameViewSet(ModelViewSet): 'name': ["This query parameter is required."] }) - def list(self, request: HttpRequest, *args, **kwargs) -> Response: + def list(self, request: Request, *args, **kwargs) -> Response: """ DRF method for listing OffTopicChannelName entries. @@ -132,4 +134,6 @@ class OffTopicChannelNameViewSet(ModelViewSet): serialized = self.serializer_class(queryset, many=True) return Response(serialized.data) - return super().list(self, request) + queryset = self.get_queryset(active=bool(request.query_params.get("active", True))) + serialized = self.serializer_class(queryset, many=True) + return Response(serialized.data) -- cgit v1.2.3 From c998d475440cf4819bad7ebc3ed19f31ce82baf4 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Tue, 8 Jun 2021 00:26:40 +0200 Subject: Move subdomains to query paths. In more detail: - Use Django URL namespaces (e.g. `api:bot:infractions`) instead of `django_hosts` host argument. - Update the hosts file setup documentation to remove subdomain entries. - Update the hosts file setup documentation to mention that the entry of `pythondiscord.local` is not required and mainly for convenience. - Rename the `APISubdomainTestCase` to the more fitting `AuthenticatedAPITestCase`, as authentication is all that is left that the class is doing. - Drop dependency to `django_hosts`. --- poetry.lock | 14 +-- pydis_site/apps/admin/urls.py | 1 + .../api/models/bot/message_deletion_context.py | 4 +- pydis_site/apps/api/tests/base.py | 27 +++--- pydis_site/apps/api/tests/test_deleted_messages.py | 16 ++-- .../apps/api/tests/test_documentation_links.py | 50 +++++----- pydis_site/apps/api/tests/test_filterlists.py | 16 ++-- pydis_site/apps/api/tests/test_healthcheck.py | 8 +- pydis_site/apps/api/tests/test_infractions.py | 104 ++++++++++----------- pydis_site/apps/api/tests/test_nominations.py | 86 ++++++++--------- .../apps/api/tests/test_off_topic_channel_names.py | 48 +++++----- .../apps/api/tests/test_offensive_message.py | 24 ++--- pydis_site/apps/api/tests/test_reminders.py | 48 +++++----- pydis_site/apps/api/tests/test_roles.py | 24 ++--- pydis_site/apps/api/tests/test_rules.py | 12 +-- pydis_site/apps/api/tests/test_users.py | 66 ++++++------- .../api/viewsets/bot/off_topic_channel_name.py | 4 +- .../guides/pydis-guides/contributing/bot.md | 1 - .../guides/pydis-guides/contributing/hosts-file.md | 7 +- .../guides/pydis-guides/contributing/site.md | 6 +- pydis_site/apps/events/tests/test_views.py | 10 +- pydis_site/apps/home/tests/test_views.py | 4 +- pydis_site/apps/home/urls.py | 8 +- pydis_site/apps/redirect/tests.py | 4 +- pydis_site/apps/resources/tests/test_views.py | 6 +- pydis_site/apps/staff/tests/test_logs_view.py | 22 ++--- pydis_site/hosts.py | 13 --- pydis_site/settings.py | 22 +---- pydis_site/templates/base/navbar.html | 2 +- pydis_site/templates/home/index.html | 4 +- pydis_site/urls.py | 16 +++- pyproject.toml | 1 - 32 files changed, 311 insertions(+), 367 deletions(-) delete mode 100644 pydis_site/hosts.py diff --git a/poetry.lock b/poetry.lock index 76c8890b..b43cd7ff 100644 --- a/poetry.lock +++ b/poetry.lock @@ -149,14 +149,6 @@ python-versions = ">=3.4" [package.dependencies] Django = ">=1.11" -[[package]] -name = "django-hosts" -version = "4.0" -description = "Dynamic and static host resolving for Django. Maps hostnames to URLconfs." -category = "main" -optional = false -python-versions = ">=3.5" - [[package]] name = "django-simple-bulma" version = "2.2.0" @@ -706,7 +698,7 @@ brotli = ["brotli"] [metadata] lock-version = "1.1" python-versions = "3.9.*" -content-hash = "741444c18250124e2d998506b0643fb26240d7481389a13648b02fa1794435e3" +content-hash = "b5750a4a7ad3e6d1f51c3053164889e0e58b004bf7f92b908cd3d6316e7e9945" [metadata.files] appdirs = [ @@ -815,10 +807,6 @@ 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"}, ] -django-hosts = [ - {file = "django-hosts-4.0.tar.gz", hash = "sha256:59a870d453f113c889a7888bae5408888870350e83e362740f382dad569c2281"}, - {file = "django_hosts-4.0-py2.py3-none-any.whl", hash = "sha256:136ac225f34e7f2c007294441a38663ec2bba9637d870ad001def81bca87e390"}, -] django-simple-bulma = [ {file = "django-simple-bulma-2.2.0.tar.gz", hash = "sha256:dfc34839e050d5e4749498806ed7ee8c77794021efa54ab224a2de925c644fea"}, {file = "django_simple_bulma-2.2.0-py3-none-any.whl", hash = "sha256:38530d787b2b6a091b480f7cbb8841a3b3709733ebfa5e543ae339c3d8fece03"}, diff --git a/pydis_site/apps/admin/urls.py b/pydis_site/apps/admin/urls.py index 146c6496..a4f3e517 100644 --- a/pydis_site/apps/admin/urls.py +++ b/pydis_site/apps/admin/urls.py @@ -2,6 +2,7 @@ from django.contrib import admin from django.urls import path +app_name = 'admin' urlpatterns = ( path('', admin.site.urls), ) diff --git a/pydis_site/apps/api/models/bot/message_deletion_context.py b/pydis_site/apps/api/models/bot/message_deletion_context.py index 1410250a..25741266 100644 --- a/pydis_site/apps/api/models/bot/message_deletion_context.py +++ b/pydis_site/apps/api/models/bot/message_deletion_context.py @@ -1,5 +1,5 @@ from django.db import models -from django_hosts.resolvers import reverse +from django.urls import reverse from pydis_site.apps.api.models.bot.user import User from pydis_site.apps.api.models.mixins import ModelReprMixin @@ -33,7 +33,7 @@ class MessageDeletionContext(ModelReprMixin, models.Model): @property def log_url(self) -> str: """Create the url for the deleted message logs.""" - return reverse('logs', host="staff", args=(self.id,)) + return reverse('staff:logs', args=(self.id,)) class Meta: """Set the ordering for list views to newest first.""" diff --git a/pydis_site/apps/api/tests/base.py b/pydis_site/apps/api/tests/base.py index 61c23b0f..c9f3cb7e 100644 --- a/pydis_site/apps/api/tests/base.py +++ b/pydis_site/apps/api/tests/base.py @@ -11,7 +11,7 @@ test_user, _created = User.objects.get_or_create( ) -class APISubdomainTestCase(APITestCase): +class AuthenticatedAPITestCase(APITestCase): """ Configures the test client. @@ -24,14 +24,13 @@ class APISubdomainTestCase(APITestCase): `self.client.force_authenticate(user=created_user)` to force authentication through the created user. - Using this performs the following niceties for you which ease writing tests: - - setting the `HTTP_HOST` request header to `api.pythondiscord.local:8000`, and + Using this performs the following nicety for you which eases writing tests: - forcing authentication for the test user. If you don't want to force authentication (for example, to test a route's response for an unauthenticated user), un-force authentication by using the following: - >>> from pydis_site.apps.api.tests.base import APISubdomainTestCase - >>> class UnauthedUserTestCase(APISubdomainTestCase): + >>> from pydis_site.apps.api.tests.base import AuthenticatedAPITestCase + >>> class UnauthedUserTestCase(AuthenticatedAPITestCase): ... def setUp(self): ... super().setUp() ... self.client.force_authentication(user=None) @@ -42,30 +41,26 @@ class APISubdomainTestCase(APITestCase): ... resp = self.client.delete('/my-publicly-readable-endpoint/42') ... self.assertEqual(resp.status_code, 401) - Make sure to include the `super().setUp(self)` call, otherwise, you may get - status code 404 for some URLs due to the missing `HTTP_HOST` header. - ## Example Using this in a test case is rather straightforward: - >>> from pydis_site.apps.api.tests.base import APISubdomainTestCase - >>> class MyAPITestCase(APISubdomainTestCase): + >>> from pydis_site.apps.api.tests.base import AuthenticatedAPITestCase + >>> class MyAPITestCase(AuthenticatedAPITestCase): ... def test_that_it_works(self): ... response = self.client.get('/my-endpoint') ... self.assertEqual(response.status_code, 200) - To reverse URLs of the API host, you need to use `django_hosts`: + To reverse URLs of the API host, you need to use `django.urls`: - >>> from django_hosts.resolvers import reverse - >>> from pydis_site.apps.api.tests.base import APISubdomainTestCase - >>> class MyReversedTestCase(APISubdomainTestCase): + >>> from django.urls import reverse + >>> from pydis_site.apps.api.tests.base import AuthenticatedAPITestCase + >>> class MyReversedTestCase(AuthenticatedAPITestCase): ... def test_my_endpoint(self): - ... url = reverse('user-detail', host='api') + ... url = reverse('api:user-detail') ... response = self.client.get(url) ... self.assertEqual(response.status_code, 200) """ def setUp(self): super().setUp() - self.client.defaults['HTTP_HOST'] = 'api.pythondiscord.local:8000' self.client.force_authenticate(test_user) diff --git a/pydis_site/apps/api/tests/test_deleted_messages.py b/pydis_site/apps/api/tests/test_deleted_messages.py index 40450844..1eb535d8 100644 --- a/pydis_site/apps/api/tests/test_deleted_messages.py +++ b/pydis_site/apps/api/tests/test_deleted_messages.py @@ -1,13 +1,13 @@ from datetime import datetime +from django.urls import reverse from django.utils import timezone -from django_hosts.resolvers import reverse -from .base import APISubdomainTestCase +from .base import AuthenticatedAPITestCase from ..models import MessageDeletionContext, User -class DeletedMessagesWithoutActorTests(APISubdomainTestCase): +class DeletedMessagesWithoutActorTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): cls.author = User.objects.create( @@ -40,14 +40,14 @@ class DeletedMessagesWithoutActorTests(APISubdomainTestCase): } def test_accepts_valid_data(self): - url = reverse('bot:messagedeletioncontext-list', host='api') + url = reverse('api:bot:messagedeletioncontext-list') response = self.client.post(url, data=self.data) self.assertEqual(response.status_code, 201) [context] = MessageDeletionContext.objects.all() self.assertIsNone(context.actor) -class DeletedMessagesWithActorTests(APISubdomainTestCase): +class DeletedMessagesWithActorTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): cls.author = cls.actor = User.objects.create( @@ -72,14 +72,14 @@ class DeletedMessagesWithActorTests(APISubdomainTestCase): } def test_accepts_valid_data_and_sets_actor(self): - url = reverse('bot:messagedeletioncontext-list', host='api') + url = reverse('api:bot:messagedeletioncontext-list') response = self.client.post(url, data=self.data) self.assertEqual(response.status_code, 201) [context] = MessageDeletionContext.objects.all() self.assertEqual(context.actor.id, self.actor.id) -class DeletedMessagesLogURLTests(APISubdomainTestCase): +class DeletedMessagesLogURLTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): cls.author = cls.actor = User.objects.create( @@ -94,6 +94,6 @@ class DeletedMessagesLogURLTests(APISubdomainTestCase): ) def test_valid_log_url(self): - expected_url = reverse('logs', host="staff", args=(1,)) + expected_url = reverse('staff:logs', args=(1,)) [context] = MessageDeletionContext.objects.all() self.assertEqual(context.log_url, expected_url) diff --git a/pydis_site/apps/api/tests/test_documentation_links.py b/pydis_site/apps/api/tests/test_documentation_links.py index 39fb08f3..4e238cbb 100644 --- a/pydis_site/apps/api/tests/test_documentation_links.py +++ b/pydis_site/apps/api/tests/test_documentation_links.py @@ -1,61 +1,61 @@ -from django_hosts.resolvers import reverse +from django.urls import reverse -from .base import APISubdomainTestCase +from .base import AuthenticatedAPITestCase from ..models import DocumentationLink -class UnauthedDocumentationLinkAPITests(APISubdomainTestCase): +class UnauthedDocumentationLinkAPITests(AuthenticatedAPITestCase): def setUp(self): super().setUp() self.client.force_authenticate(user=None) def test_detail_lookup_returns_401(self): - url = reverse('bot:documentationlink-detail', args=('whatever',), host='api') + url = reverse('api:bot:documentationlink-detail', args=('whatever',)) response = self.client.get(url) self.assertEqual(response.status_code, 401) def test_list_returns_401(self): - url = reverse('bot:documentationlink-list', host='api') + url = reverse('api:bot:documentationlink-list') response = self.client.get(url) self.assertEqual(response.status_code, 401) def test_create_returns_401(self): - url = reverse('bot:documentationlink-list', host='api') + url = reverse('api:bot:documentationlink-list') response = self.client.post(url, data={'hi': 'there'}) self.assertEqual(response.status_code, 401) def test_delete_returns_401(self): - url = reverse('bot:documentationlink-detail', args=('whatever',), host='api') + url = reverse('api:bot:documentationlink-detail', args=('whatever',)) response = self.client.delete(url) self.assertEqual(response.status_code, 401) -class EmptyDatabaseDocumentationLinkAPITests(APISubdomainTestCase): +class EmptyDatabaseDocumentationLinkAPITests(AuthenticatedAPITestCase): def test_detail_lookup_returns_404(self): - url = reverse('bot:documentationlink-detail', args=('whatever',), host='api') + url = reverse('api:bot:documentationlink-detail', args=('whatever',)) response = self.client.get(url) self.assertEqual(response.status_code, 404) def test_list_all_returns_empty_list(self): - url = reverse('bot:documentationlink-list', host='api') + url = reverse('api:bot:documentationlink-list') response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), []) def test_delete_returns_404(self): - url = reverse('bot:documentationlink-detail', args=('whatever',), host='api') + url = reverse('api:bot:documentationlink-detail', args=('whatever',)) response = self.client.delete(url) self.assertEqual(response.status_code, 404) -class DetailLookupDocumentationLinkAPITests(APISubdomainTestCase): +class DetailLookupDocumentationLinkAPITests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): cls.doc_link = DocumentationLink.objects.create( @@ -71,27 +71,27 @@ class DetailLookupDocumentationLinkAPITests(APISubdomainTestCase): } def test_detail_lookup_unknown_package_returns_404(self): - url = reverse('bot:documentationlink-detail', args=('whatever',), host='api') + url = reverse('api:bot:documentationlink-detail', args=('whatever',)) response = self.client.get(url) self.assertEqual(response.status_code, 404) def test_detail_lookup_created_package_returns_package(self): - url = reverse('bot:documentationlink-detail', args=(self.doc_link.package,), host='api') + url = reverse('api:bot:documentationlink-detail', args=(self.doc_link.package,)) response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), self.doc_json) def test_list_all_packages_shows_created_package(self): - url = reverse('bot:documentationlink-list', host='api') + url = reverse('api:bot:documentationlink-list') response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), [self.doc_json]) def test_create_invalid_body_returns_400(self): - url = reverse('bot:documentationlink-list', host='api') + url = reverse('api:bot:documentationlink-list') response = self.client.post(url, data={'i': 'am', 'totally': 'valid'}) self.assertEqual(response.status_code, 400) @@ -103,7 +103,7 @@ class DetailLookupDocumentationLinkAPITests(APISubdomainTestCase): 'inventory_url': 'totally an url' } - url = reverse('bot:documentationlink-list', host='api') + url = reverse('api:bot:documentationlink-list') response = self.client.post(url, data=body) self.assertEqual(response.status_code, 400) @@ -114,13 +114,13 @@ class DetailLookupDocumentationLinkAPITests(APISubdomainTestCase): with self.subTest(package_name=case): body = self.doc_json.copy() body['package'] = case - url = reverse('bot:documentationlink-list', host='api') + url = reverse('api:bot:documentationlink-list') response = self.client.post(url, data=body) self.assertEqual(response.status_code, 400) -class DocumentationLinkCreationTests(APISubdomainTestCase): +class DocumentationLinkCreationTests(AuthenticatedAPITestCase): def setUp(self): super().setUp() @@ -130,27 +130,27 @@ class DocumentationLinkCreationTests(APISubdomainTestCase): 'inventory_url': 'https://docs.example.com' } - url = reverse('bot:documentationlink-list', host='api') + url = reverse('api:bot:documentationlink-list') response = self.client.post(url, data=self.body) self.assertEqual(response.status_code, 201) def test_package_in_full_list(self): - url = reverse('bot:documentationlink-list', host='api') + url = reverse('api:bot:documentationlink-list') response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), [self.body]) def test_detail_lookup_works_with_package(self): - url = reverse('bot:documentationlink-detail', args=(self.body['package'],), host='api') + url = reverse('api:bot:documentationlink-detail', args=(self.body['package'],)) response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), self.body) -class DocumentationLinkDeletionTests(APISubdomainTestCase): +class DocumentationLinkDeletionTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): cls.doc_link = DocumentationLink.objects.create( @@ -160,13 +160,13 @@ class DocumentationLinkDeletionTests(APISubdomainTestCase): ) def test_unknown_package_returns_404(self): - url = reverse('bot:documentationlink-detail', args=('whatever',), host='api') + url = reverse('api:bot:documentationlink-detail', args=('whatever',)) response = self.client.delete(url) self.assertEqual(response.status_code, 404) def test_delete_known_package_returns_204(self): - url = reverse('bot:documentationlink-detail', args=(self.doc_link.package,), host='api') + url = reverse('api:bot:documentationlink-detail', args=(self.doc_link.package,)) response = self.client.delete(url) self.assertEqual(response.status_code, 204) diff --git a/pydis_site/apps/api/tests/test_filterlists.py b/pydis_site/apps/api/tests/test_filterlists.py index 188c0fff..5a5bca60 100644 --- a/pydis_site/apps/api/tests/test_filterlists.py +++ b/pydis_site/apps/api/tests/test_filterlists.py @@ -1,9 +1,9 @@ -from django_hosts.resolvers import reverse +from django.urls import reverse from pydis_site.apps.api.models import FilterList -from pydis_site.apps.api.tests.base import APISubdomainTestCase +from pydis_site.apps.api.tests.base import AuthenticatedAPITestCase -URL = reverse('bot:filterlist-list', host='api') +URL = reverse('api:bot:filterlist-list') JPEG_ALLOWLIST = { "type": 'FILE_FORMAT', "allowed": True, @@ -16,7 +16,7 @@ PNG_ALLOWLIST = { } -class UnauthenticatedTests(APISubdomainTestCase): +class UnauthenticatedTests(AuthenticatedAPITestCase): def setUp(self): super().setUp() self.client.force_authenticate(user=None) @@ -27,7 +27,7 @@ class UnauthenticatedTests(APISubdomainTestCase): self.assertEqual(response.status_code, 401) -class EmptyDatabaseTests(APISubdomainTestCase): +class EmptyDatabaseTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): FilterList.objects.all().delete() @@ -39,7 +39,7 @@ class EmptyDatabaseTests(APISubdomainTestCase): self.assertEqual(response.json(), []) -class FetchTests(APISubdomainTestCase): +class FetchTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): FilterList.objects.all().delete() @@ -68,7 +68,7 @@ class FetchTests(APISubdomainTestCase): self.assertEquals(api_type[1], model_type[1]) -class CreationTests(APISubdomainTestCase): +class CreationTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): FilterList.objects.all().delete() @@ -103,7 +103,7 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response.status_code, 400) -class DeletionTests(APISubdomainTestCase): +class DeletionTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): FilterList.objects.all().delete() diff --git a/pydis_site/apps/api/tests/test_healthcheck.py b/pydis_site/apps/api/tests/test_healthcheck.py index b0fd71bf..650403ad 100644 --- a/pydis_site/apps/api/tests/test_healthcheck.py +++ b/pydis_site/apps/api/tests/test_healthcheck.py @@ -1,15 +1,15 @@ -from django_hosts.resolvers import reverse +from django.urls import reverse -from .base import APISubdomainTestCase +from .base import AuthenticatedAPITestCase -class UnauthedHealthcheckAPITests(APISubdomainTestCase): +class UnauthedHealthcheckAPITests(AuthenticatedAPITestCase): def setUp(self): super().setUp() self.client.force_authenticate(user=None) def test_can_access_healthcheck_view(self): - url = reverse('healthcheck', host='api') + url = reverse('api:healthcheck') response = self.client.get(url) self.assertEqual(response.status_code, 200) diff --git a/pydis_site/apps/api/tests/test_infractions.py b/pydis_site/apps/api/tests/test_infractions.py index 9aae16c0..b3dd16ee 100644 --- a/pydis_site/apps/api/tests/test_infractions.py +++ b/pydis_site/apps/api/tests/test_infractions.py @@ -4,44 +4,44 @@ from unittest.mock import patch from urllib.parse import quote from django.db.utils import IntegrityError -from django_hosts.resolvers import reverse +from django.urls import reverse -from .base import APISubdomainTestCase +from .base import AuthenticatedAPITestCase from ..models import Infraction, User from ..serializers import InfractionSerializer -class UnauthenticatedTests(APISubdomainTestCase): +class UnauthenticatedTests(AuthenticatedAPITestCase): def setUp(self): super().setUp() self.client.force_authenticate(user=None) def test_detail_lookup_returns_401(self): - url = reverse('bot:infraction-detail', args=(6,), host='api') + url = reverse('api:bot:infraction-detail', args=(6,)) response = self.client.get(url) self.assertEqual(response.status_code, 401) def test_list_returns_401(self): - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') response = self.client.get(url) self.assertEqual(response.status_code, 401) def test_create_returns_401(self): - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') response = self.client.post(url, data={'reason': 'Have a nice day.'}) self.assertEqual(response.status_code, 401) def test_partial_update_returns_401(self): - url = reverse('bot:infraction-detail', args=(6,), host='api') + url = reverse('api:bot:infraction-detail', args=(6,)) response = self.client.patch(url, data={'reason': 'Have a nice day.'}) self.assertEqual(response.status_code, 401) -class InfractionTests(APISubdomainTestCase): +class InfractionTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): cls.user = User.objects.create( @@ -92,7 +92,7 @@ class InfractionTests(APISubdomainTestCase): def test_list_all(self): """Tests the list-view, which should be ordered by inserted_at (newest first).""" - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -106,7 +106,7 @@ class InfractionTests(APISubdomainTestCase): self.assertEqual(infractions[4]['id'], self.ban_hidden.id) def test_filter_search(self): - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') pattern = quote(r'^James(\s\w+){3},') response = self.client.get(f'{url}?search={pattern}') @@ -117,7 +117,7 @@ class InfractionTests(APISubdomainTestCase): self.assertEqual(infractions[0]['id'], self.ban_inactive.id) def test_filter_field(self): - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') response = self.client.get(f'{url}?type=ban&hidden=true') self.assertEqual(response.status_code, 200) @@ -127,7 +127,7 @@ class InfractionTests(APISubdomainTestCase): self.assertEqual(infractions[0]['id'], self.ban_hidden.id) def test_filter_permanent_false(self): - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') response = self.client.get(f'{url}?type=mute&permanent=false') self.assertEqual(response.status_code, 200) @@ -136,7 +136,7 @@ class InfractionTests(APISubdomainTestCase): self.assertEqual(len(infractions), 0) def test_filter_permanent_true(self): - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') response = self.client.get(f'{url}?type=mute&permanent=true') self.assertEqual(response.status_code, 200) @@ -145,7 +145,7 @@ class InfractionTests(APISubdomainTestCase): self.assertEqual(infractions[0]['id'], self.mute_permanent.id) def test_filter_after(self): - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') target_time = datetime.datetime.utcnow() + datetime.timedelta(hours=5) response = self.client.get(f'{url}?type=superstar&expires_after={target_time.isoformat()}') @@ -154,7 +154,7 @@ class InfractionTests(APISubdomainTestCase): self.assertEqual(len(infractions), 0) def test_filter_before(self): - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') target_time = datetime.datetime.utcnow() + datetime.timedelta(hours=5) response = self.client.get(f'{url}?type=superstar&expires_before={target_time.isoformat()}') @@ -164,21 +164,21 @@ class InfractionTests(APISubdomainTestCase): self.assertEqual(infractions[0]['id'], self.superstar_expires_soon.id) def test_filter_after_invalid(self): - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') response = self.client.get(f'{url}?expires_after=gibberish') self.assertEqual(response.status_code, 400) self.assertEqual(list(response.json())[0], "expires_after") def test_filter_before_invalid(self): - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') response = self.client.get(f'{url}?expires_before=000000000') self.assertEqual(response.status_code, 400) self.assertEqual(list(response.json())[0], "expires_before") def test_after_before_before(self): - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') target_time = datetime.datetime.utcnow() + datetime.timedelta(hours=4) target_time_late = datetime.datetime.utcnow() + datetime.timedelta(hours=6) response = self.client.get( @@ -191,7 +191,7 @@ class InfractionTests(APISubdomainTestCase): self.assertEqual(response.json()[0]["id"], self.superstar_expires_soon.id) def test_after_after_before_invalid(self): - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') target_time = datetime.datetime.utcnow() + datetime.timedelta(hours=5) target_time_late = datetime.datetime.utcnow() + datetime.timedelta(hours=9) response = self.client.get( @@ -205,7 +205,7 @@ class InfractionTests(APISubdomainTestCase): self.assertIn("expires_after", errors) def test_permanent_after_invalid(self): - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') target_time = datetime.datetime.utcnow() + datetime.timedelta(hours=5) response = self.client.get(f'{url}?permanent=true&expires_after={target_time.isoformat()}') @@ -214,7 +214,7 @@ class InfractionTests(APISubdomainTestCase): self.assertEqual("permanent", errors[0]) def test_permanent_before_invalid(self): - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') target_time = datetime.datetime.utcnow() + datetime.timedelta(hours=5) response = self.client.get(f'{url}?permanent=true&expires_before={target_time.isoformat()}') @@ -223,7 +223,7 @@ class InfractionTests(APISubdomainTestCase): self.assertEqual("permanent", errors[0]) def test_nonpermanent_before(self): - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') target_time = datetime.datetime.utcnow() + datetime.timedelta(hours=6) response = self.client.get( f'{url}?permanent=false&expires_before={target_time.isoformat()}' @@ -234,7 +234,7 @@ class InfractionTests(APISubdomainTestCase): self.assertEqual(response.json()[0]["id"], self.superstar_expires_soon.id) def test_filter_manytypes(self): - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') response = self.client.get(f'{url}?types=mute,ban') self.assertEqual(response.status_code, 200) @@ -242,7 +242,7 @@ class InfractionTests(APISubdomainTestCase): self.assertEqual(len(infractions), 3) def test_types_type_invalid(self): - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') response = self.client.get(f'{url}?types=mute,ban&type=superstar') self.assertEqual(response.status_code, 400) @@ -250,7 +250,7 @@ class InfractionTests(APISubdomainTestCase): self.assertEqual("types", errors[0]) def test_sort_expiresby(self): - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') response = self.client.get(f'{url}?ordering=expires_at&permanent=false') self.assertEqual(response.status_code, 200) infractions = response.json() @@ -261,34 +261,34 @@ class InfractionTests(APISubdomainTestCase): self.assertEqual(infractions[2]['id'], self.ban_hidden.id) def test_returns_empty_for_no_match(self): - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') response = self.client.get(f'{url}?type=ban&search=poop') self.assertEqual(response.status_code, 200) self.assertEqual(len(response.json()), 0) def test_ignores_bad_filters(self): - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') response = self.client.get(f'{url}?type=ban&hidden=maybe&foo=bar') self.assertEqual(response.status_code, 200) self.assertEqual(len(response.json()), 2) def test_retrieve_single_from_id(self): - url = reverse('bot:infraction-detail', args=(self.ban_inactive.id,), host='api') + url = reverse('api:bot:infraction-detail', args=(self.ban_inactive.id,)) response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertEqual(response.json()['id'], self.ban_inactive.id) def test_retrieve_returns_404_for_absent_id(self): - url = reverse('bot:infraction-detail', args=(1337,), host='api') + url = reverse('api:bot:infraction-detail', args=(1337,)) response = self.client.get(url) self.assertEqual(response.status_code, 404) def test_partial_update(self): - url = reverse('bot:infraction-detail', args=(self.ban_hidden.id,), host='api') + url = reverse('api:bot:infraction-detail', args=(self.ban_hidden.id,)) data = { 'expires_at': '4143-02-15T21:04:31+00:00', 'active': False, @@ -313,7 +313,7 @@ class InfractionTests(APISubdomainTestCase): self.assertEqual(infraction.hidden, self.ban_hidden.hidden) def test_partial_update_returns_400_for_frozen_field(self): - url = reverse('bot:infraction-detail', args=(self.ban_hidden.id,), host='api') + url = reverse('api:bot:infraction-detail', args=(self.ban_hidden.id,)) data = {'user': 6} response = self.client.patch(url, data=data) @@ -323,7 +323,7 @@ class InfractionTests(APISubdomainTestCase): }) -class CreationTests(APISubdomainTestCase): +class CreationTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): cls.user = User.objects.create( @@ -338,7 +338,7 @@ class CreationTests(APISubdomainTestCase): ) def test_accepts_valid_data(self): - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') data = { 'user': self.user.id, 'actor': self.user.id, @@ -367,7 +367,7 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(infraction.active, True) def test_returns_400_for_missing_user(self): - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') data = { 'actor': self.user.id, 'type': 'kick', @@ -381,7 +381,7 @@ class CreationTests(APISubdomainTestCase): }) def test_returns_400_for_bad_user(self): - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') data = { 'user': 1337, 'actor': self.user.id, @@ -396,7 +396,7 @@ class CreationTests(APISubdomainTestCase): }) def test_returns_400_for_bad_type(self): - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') data = { 'user': self.user.id, 'actor': self.user.id, @@ -411,7 +411,7 @@ class CreationTests(APISubdomainTestCase): }) def test_returns_400_for_bad_expired_at_format(self): - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') data = { 'user': self.user.id, 'actor': self.user.id, @@ -430,7 +430,7 @@ class CreationTests(APISubdomainTestCase): }) def test_returns_400_for_expiring_non_expirable_type(self): - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') for infraction_type in ('kick', 'warning'): data = { @@ -448,7 +448,7 @@ class CreationTests(APISubdomainTestCase): }) def test_returns_400_for_hidden_non_hideable_type(self): - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') for infraction_type in ('superstar', 'warning'): data = { @@ -466,7 +466,7 @@ class CreationTests(APISubdomainTestCase): }) def test_returns_400_for_non_hidden_required_hidden_type(self): - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') data = { 'user': self.user.id, @@ -484,7 +484,7 @@ class CreationTests(APISubdomainTestCase): def test_returns_400_for_active_infraction_of_type_that_cannot_be_active(self): """Test if the API rejects active infractions for types that cannot be active.""" - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') restricted_types = ( ('note', True), ('warning', False), @@ -511,7 +511,7 @@ class CreationTests(APISubdomainTestCase): def test_returns_400_for_second_active_infraction_of_the_same_type(self): """Test if the API rejects a second active infraction of the same type for a given user.""" - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') active_infraction_types = ('mute', 'ban', 'superstar') for infraction_type in active_infraction_types: @@ -550,7 +550,7 @@ class CreationTests(APISubdomainTestCase): def test_returns_201_for_second_active_infraction_of_different_type(self): """Test if the API accepts a second active infraction of a different type than the first.""" - url = reverse('bot:infraction-list', host='api') + url = reverse('api:bot:infraction-list') first_active_infraction = { 'user': self.user.id, 'actor': self.user.id, @@ -677,7 +677,7 @@ class CreationTests(APISubdomainTestCase): ) -class InfractionDeletionTests(APISubdomainTestCase): +class InfractionDeletionTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): cls.user = User.objects.create( @@ -694,20 +694,20 @@ class InfractionDeletionTests(APISubdomainTestCase): ) def test_delete_unknown_infraction_returns_404(self): - url = reverse('bot:infraction-detail', args=('something',), host='api') + url = reverse('api:bot:infraction-detail', args=('something',)) response = self.client.delete(url) self.assertEqual(response.status_code, 404) def test_delete_known_infraction_returns_204(self): - url = reverse('bot:infraction-detail', args=(self.warning.id,), host='api') + url = reverse('api:bot:infraction-detail', args=(self.warning.id,)) response = self.client.delete(url) self.assertEqual(response.status_code, 204) self.assertRaises(Infraction.DoesNotExist, Infraction.objects.get, id=self.warning.id) -class ExpandedTests(APISubdomainTestCase): +class ExpandedTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): cls.user = User.objects.create( @@ -735,7 +735,7 @@ class ExpandedTests(APISubdomainTestCase): self.assertTrue(field in obj, msg=f'field "{field}" missing from {key}') def test_list_expanded(self): - url = reverse('bot:infraction-list-expanded', host='api') + url = reverse('api:bot:infraction-list-expanded') response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -747,7 +747,7 @@ class ExpandedTests(APISubdomainTestCase): self.check_expanded_fields(infraction) def test_create_expanded(self): - url = reverse('bot:infraction-list-expanded', host='api') + url = reverse('api:bot:infraction-list-expanded') data = { 'user': self.user.id, 'actor': self.user.id, @@ -762,7 +762,7 @@ class ExpandedTests(APISubdomainTestCase): self.check_expanded_fields(response.json()) def test_retrieve_expanded(self): - url = reverse('bot:infraction-detail-expanded', args=(self.warning.id,), host='api') + url = reverse('api:bot:infraction-detail-expanded', args=(self.warning.id,)) response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -772,7 +772,7 @@ class ExpandedTests(APISubdomainTestCase): self.check_expanded_fields(infraction) def test_partial_update_expanded(self): - url = reverse('bot:infraction-detail-expanded', args=(self.kick.id,), host='api') + url = reverse('api:bot:infraction-detail-expanded', args=(self.kick.id,)) data = {'active': False} response = self.client.patch(url, data=data) @@ -783,7 +783,7 @@ class ExpandedTests(APISubdomainTestCase): self.check_expanded_fields(response.json()) -class SerializerTests(APISubdomainTestCase): +class SerializerTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): cls.user = User.objects.create( diff --git a/pydis_site/apps/api/tests/test_nominations.py b/pydis_site/apps/api/tests/test_nominations.py index 9cefbd8f..62b2314c 100644 --- a/pydis_site/apps/api/tests/test_nominations.py +++ b/pydis_site/apps/api/tests/test_nominations.py @@ -1,12 +1,12 @@ from datetime import datetime as dt, timedelta, timezone -from django_hosts.resolvers import reverse +from django.urls import reverse -from .base import APISubdomainTestCase +from .base import AuthenticatedAPITestCase from ..models import Nomination, NominationEntry, User -class CreationTests(APISubdomainTestCase): +class CreationTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): cls.user = User.objects.create( @@ -21,7 +21,7 @@ class CreationTests(APISubdomainTestCase): ) def test_accepts_valid_data(self): - url = reverse('bot:nomination-list', host='api') + url = reverse('api:bot:nomination-list') data = { 'actor': self.user.id, 'reason': 'Joe Dart on Fender Bass', @@ -46,7 +46,7 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(nomination.active, True) def test_returns_200_on_second_active_nomination_by_different_user(self): - url = reverse('bot:nomination-list', host='api') + url = reverse('api:bot:nomination-list') first_data = { 'actor': self.user.id, 'reason': 'Joe Dart on Fender Bass', @@ -65,7 +65,7 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response2.status_code, 201) def test_returns_400_on_second_active_nomination_by_existing_nominator(self): - url = reverse('bot:nomination-list', host='api') + url = reverse('api:bot:nomination-list') data = { 'actor': self.user.id, 'reason': 'Joe Dart on Fender Bass', @@ -82,7 +82,7 @@ class CreationTests(APISubdomainTestCase): }) def test_returns_400_for_missing_user(self): - url = reverse('bot:nomination-list', host='api') + url = reverse('api:bot:nomination-list') data = { 'actor': self.user.id, 'reason': 'Joe Dart on Fender Bass', @@ -95,7 +95,7 @@ class CreationTests(APISubdomainTestCase): }) def test_returns_400_for_missing_actor(self): - url = reverse('bot:nomination-list', host='api') + url = reverse('api:bot:nomination-list') data = { 'user': self.user.id, 'reason': 'Joe Dart on Fender Bass', @@ -108,7 +108,7 @@ class CreationTests(APISubdomainTestCase): }) def test_returns_201_for_missing_reason(self): - url = reverse('bot:nomination-list', host='api') + url = reverse('api:bot:nomination-list') data = { 'user': self.user.id, 'actor': self.user.id, @@ -118,7 +118,7 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response.status_code, 201) def test_returns_400_for_bad_user(self): - url = reverse('bot:nomination-list', host='api') + url = reverse('api:bot:nomination-list') data = { 'user': 1024, 'reason': 'Joe Dart on Fender Bass', @@ -132,7 +132,7 @@ class CreationTests(APISubdomainTestCase): }) def test_returns_400_for_bad_actor(self): - url = reverse('bot:nomination-list', host='api') + url = reverse('api:bot:nomination-list') data = { 'user': self.user.id, 'reason': 'Joe Dart on Fender Bass', @@ -146,7 +146,7 @@ class CreationTests(APISubdomainTestCase): }) def test_returns_400_for_end_reason_at_creation(self): - url = reverse('bot:nomination-list', host='api') + url = reverse('api:bot:nomination-list') data = { 'user': self.user.id, 'reason': 'Joe Dart on Fender Bass', @@ -161,7 +161,7 @@ class CreationTests(APISubdomainTestCase): }) def test_returns_400_for_ended_at_at_creation(self): - url = reverse('bot:nomination-list', host='api') + url = reverse('api:bot:nomination-list') data = { 'user': self.user.id, 'reason': 'Joe Dart on Fender Bass', @@ -176,7 +176,7 @@ class CreationTests(APISubdomainTestCase): }) def test_returns_400_for_inserted_at_at_creation(self): - url = reverse('bot:nomination-list', host='api') + url = reverse('api:bot:nomination-list') data = { 'user': self.user.id, 'reason': 'Joe Dart on Fender Bass', @@ -191,7 +191,7 @@ class CreationTests(APISubdomainTestCase): }) def test_returns_400_for_active_at_creation(self): - url = reverse('bot:nomination-list', host='api') + url = reverse('api:bot:nomination-list') data = { 'user': self.user.id, 'reason': 'Joe Dart on Fender Bass', @@ -206,7 +206,7 @@ class CreationTests(APISubdomainTestCase): }) -class NominationTests(APISubdomainTestCase): +class NominationTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): cls.user = User.objects.create( @@ -236,7 +236,7 @@ class NominationTests(APISubdomainTestCase): ) def test_returns_200_update_reason_on_active_with_actor(self): - url = reverse('bot:nomination-detail', args=(self.active_nomination.id,), host='api') + url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,)) data = { 'reason': "He's one funky duck", 'actor': self.user.id @@ -252,7 +252,7 @@ class NominationTests(APISubdomainTestCase): self.assertEqual(nomination_entry.reason, data['reason']) def test_returns_400_on_frozen_field_update(self): - url = reverse('bot:nomination-detail', args=(self.active_nomination.id,), host='api') + url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,)) data = { 'user': "Theo Katzman" } @@ -264,7 +264,7 @@ class NominationTests(APISubdomainTestCase): }) def test_returns_400_update_end_reason_on_active(self): - url = reverse('bot:nomination-detail', args=(self.active_nomination.id,), host='api') + url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,)) data = { 'end_reason': 'He started playing jazz' } @@ -276,7 +276,7 @@ class NominationTests(APISubdomainTestCase): }) def test_returns_200_update_reason_on_inactive(self): - url = reverse('bot:nomination-detail', args=(self.inactive_nomination.id,), host='api') + url = reverse('api:bot:nomination-detail', args=(self.inactive_nomination.id,)) data = { 'reason': "He's one funky duck", 'actor': self.user.id @@ -292,7 +292,7 @@ class NominationTests(APISubdomainTestCase): self.assertEqual(nomination_entry.reason, data['reason']) def test_returns_200_update_end_reason_on_inactive(self): - url = reverse('bot:nomination-detail', args=(self.inactive_nomination.id,), host='api') + url = reverse('api:bot:nomination-detail', args=(self.inactive_nomination.id,)) data = { 'end_reason': 'He started playing jazz' } @@ -305,9 +305,8 @@ class NominationTests(APISubdomainTestCase): def test_returns_200_on_valid_end_nomination(self): url = reverse( - 'bot:nomination-detail', + 'api:bot:nomination-detail', args=(self.active_nomination.id,), - host='api' ) data = { 'active': False, @@ -328,9 +327,8 @@ class NominationTests(APISubdomainTestCase): def test_returns_400_on_invalid_field_end_nomination(self): url = reverse( - 'bot:nomination-detail', + 'api:bot:nomination-detail', args=(self.active_nomination.id,), - host='api' ) data = { 'active': False, @@ -344,9 +342,8 @@ class NominationTests(APISubdomainTestCase): def test_returns_400_on_missing_end_reason_end_nomination(self): url = reverse( - 'bot:nomination-detail', + 'api:bot:nomination-detail', args=(self.active_nomination.id,), - host='api' ) data = { 'active': False, @@ -360,9 +357,8 @@ class NominationTests(APISubdomainTestCase): def test_returns_400_on_invalid_use_of_active(self): url = reverse( - 'bot:nomination-detail', + 'api:bot:nomination-detail', args=(self.inactive_nomination.id,), - host='api' ) data = { 'active': False, @@ -376,9 +372,8 @@ class NominationTests(APISubdomainTestCase): def test_returns_404_on_get_unknown_nomination(self): url = reverse( - 'bot:nomination-detail', + 'api:bot:nomination-detail', args=(9999,), - host='api' ) response = self.client.get(url, data={}) @@ -389,9 +384,8 @@ class NominationTests(APISubdomainTestCase): def test_returns_404_on_patch_unknown_nomination(self): url = reverse( - 'bot:nomination-detail', + 'api:bot:nomination-detail', args=(9999,), - host='api' ) response = self.client.patch(url, data={}) @@ -401,7 +395,7 @@ class NominationTests(APISubdomainTestCase): }) def test_returns_405_on_list_put(self): - url = reverse('bot:nomination-list', host='api') + url = reverse('api:bot:nomination-list') response = self.client.put(url, data={}) self.assertEqual(response.status_code, 405) @@ -410,7 +404,7 @@ class NominationTests(APISubdomainTestCase): }) def test_returns_405_on_list_patch(self): - url = reverse('bot:nomination-list', host='api') + url = reverse('api:bot:nomination-list') response = self.client.patch(url, data={}) self.assertEqual(response.status_code, 405) @@ -419,7 +413,7 @@ class NominationTests(APISubdomainTestCase): }) def test_returns_405_on_list_delete(self): - url = reverse('bot:nomination-list', host='api') + url = reverse('api:bot:nomination-list') response = self.client.delete(url, data={}) self.assertEqual(response.status_code, 405) @@ -428,7 +422,7 @@ class NominationTests(APISubdomainTestCase): }) def test_returns_405_on_detail_post(self): - url = reverse('bot:nomination-detail', args=(self.active_nomination.id,), host='api') + url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,)) response = self.client.post(url, data={}) self.assertEqual(response.status_code, 405) @@ -437,7 +431,7 @@ class NominationTests(APISubdomainTestCase): }) def test_returns_405_on_detail_delete(self): - url = reverse('bot:nomination-detail', args=(self.active_nomination.id,), host='api') + url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,)) response = self.client.delete(url, data={}) self.assertEqual(response.status_code, 405) @@ -446,7 +440,7 @@ class NominationTests(APISubdomainTestCase): }) def test_returns_405_on_detail_put(self): - url = reverse('bot:nomination-detail', args=(self.active_nomination.id,), host='api') + url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,)) response = self.client.put(url, data={}) self.assertEqual(response.status_code, 405) @@ -455,7 +449,7 @@ class NominationTests(APISubdomainTestCase): }) def test_filter_returns_0_objects_unknown_user__id(self): - url = reverse('bot:nomination-list', host='api') + url = reverse('api:bot:nomination-list') response = self.client.get( url, @@ -470,7 +464,7 @@ class NominationTests(APISubdomainTestCase): self.assertEqual(len(infractions), 0) def test_filter_returns_2_objects_for_testdata(self): - url = reverse('bot:nomination-list', host='api') + url = reverse('api:bot:nomination-list') response = self.client.get( url, @@ -485,14 +479,14 @@ class NominationTests(APISubdomainTestCase): self.assertEqual(len(infractions), 2) def test_patch_nomination_set_reviewed_of_active_nomination(self): - url = reverse('api:nomination-detail', args=(self.active_nomination.id,), host='api') + url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,)) data = {'reviewed': True} response = self.client.patch(url, data=data) self.assertEqual(response.status_code, 200) def test_patch_nomination_set_reviewed_of_inactive_nomination(self): - url = reverse('api:nomination-detail', args=(self.inactive_nomination.id,), host='api') + url = reverse('api:bot:nomination-detail', args=(self.inactive_nomination.id,)) data = {'reviewed': True} response = self.client.patch(url, data=data) @@ -502,7 +496,7 @@ class NominationTests(APISubdomainTestCase): }) def test_patch_nomination_set_reviewed_and_end(self): - url = reverse('api:nomination-detail', args=(self.active_nomination.id,), host='api') + url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,)) data = {'reviewed': True, 'active': False, 'end_reason': "What?"} response = self.client.patch(url, data=data) @@ -512,7 +506,7 @@ class NominationTests(APISubdomainTestCase): }) def test_modifying_reason_without_actor(self): - url = reverse('api:nomination-detail', args=(self.active_nomination.id,), host='api') + url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,)) data = {'reason': 'That is my reason!'} response = self.client.patch(url, data=data) @@ -522,7 +516,7 @@ class NominationTests(APISubdomainTestCase): }) def test_modifying_reason_with_unknown_actor(self): - url = reverse('api:nomination-detail', args=(self.active_nomination.id,), host='api') + url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,)) data = {'reason': 'That is my reason!', 'actor': 90909090909090} response = self.client.patch(url, data=data) 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 3ab8b22d..63993978 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 @@ -1,33 +1,33 @@ -from django_hosts.resolvers import reverse +from django.urls import reverse -from .base import APISubdomainTestCase +from .base import AuthenticatedAPITestCase from ..models import OffTopicChannelName -class UnauthenticatedTests(APISubdomainTestCase): +class UnauthenticatedTests(AuthenticatedAPITestCase): def setUp(self): super().setUp() self.client.force_authenticate(user=None) def test_cannot_read_off_topic_channel_name_list(self): """Return a 401 response when not authenticated.""" - url = reverse('bot:offtopicchannelname-list', host='api') + url = reverse('api:bot:offtopicchannelname-list') response = self.client.get(url) self.assertEqual(response.status_code, 401) def test_cannot_read_off_topic_channel_name_list_with_random_item_param(self): """Return a 401 response when `random_items` provided and not authenticated.""" - url = reverse('bot:offtopicchannelname-list', host='api') + url = reverse('api:bot:offtopicchannelname-list') response = self.client.get(f'{url}?random_items=no') self.assertEqual(response.status_code, 401) -class EmptyDatabaseTests(APISubdomainTestCase): +class EmptyDatabaseTests(AuthenticatedAPITestCase): def test_returns_empty_object(self): """Return empty list when no names in database.""" - url = reverse('bot:offtopicchannelname-list', host='api') + url = reverse('api:bot:offtopicchannelname-list') response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -35,7 +35,7 @@ class EmptyDatabaseTests(APISubdomainTestCase): def test_returns_empty_list_with_get_all_param(self): """Return empty list when no names and `random_items` param provided.""" - url = reverse('bot:offtopicchannelname-list', host='api') + url = reverse('api:bot:offtopicchannelname-list') response = self.client.get(f'{url}?random_items=5') self.assertEqual(response.status_code, 200) @@ -43,7 +43,7 @@ class EmptyDatabaseTests(APISubdomainTestCase): def test_returns_400_for_bad_random_items_param(self): """Return error message when passing not integer as `random_items`.""" - url = reverse('bot:offtopicchannelname-list', host='api') + url = reverse('api:bot:offtopicchannelname-list') response = self.client.get(f'{url}?random_items=totally-a-valid-integer') self.assertEqual(response.status_code, 400) @@ -53,7 +53,7 @@ class EmptyDatabaseTests(APISubdomainTestCase): def test_returns_400_for_negative_random_items_param(self): """Return error message when passing negative int as `random_items`.""" - url = reverse('bot:offtopicchannelname-list', host='api') + url = reverse('api:bot:offtopicchannelname-list') response = self.client.get(f'{url}?random_items=-5') self.assertEqual(response.status_code, 400) @@ -62,7 +62,7 @@ class EmptyDatabaseTests(APISubdomainTestCase): }) -class ListTests(APISubdomainTestCase): +class ListTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand', used=False) @@ -70,7 +70,7 @@ class ListTests(APISubdomainTestCase): def test_returns_name_in_list(self): """Return all off-topic channel names.""" - url = reverse('bot:offtopicchannelname-list', host='api') + url = reverse('api:bot:offtopicchannelname-list') response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -84,7 +84,7 @@ class ListTests(APISubdomainTestCase): def test_returns_single_item_with_random_items_param_set_to_1(self): """Return not-used name instead used.""" - url = reverse('bot:offtopicchannelname-list', host='api') + url = reverse('api:bot:offtopicchannelname-list') response = self.client.get(f'{url}?random_items=1') self.assertEqual(response.status_code, 200) @@ -93,25 +93,25 @@ class ListTests(APISubdomainTestCase): def test_running_out_of_names_with_random_parameter(self): """Reset names `used` parameter to `False` when running out of names.""" - url = reverse('bot:offtopicchannelname-list', host='api') + url = reverse('api:bot:offtopicchannelname-list') response = self.client.get(f'{url}?random_items=2') self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), [self.test_name.name, self.test_name_2.name]) -class CreationTests(APISubdomainTestCase): +class CreationTests(AuthenticatedAPITestCase): def setUp(self): super().setUp() - url = reverse('bot:offtopicchannelname-list', host='api') + url = reverse('api:bot:offtopicchannelname-list') self.name = "abcdefghijklmnopqrstuvwxyz-0123456789" response = self.client.post(f'{url}?name={self.name}') self.assertEqual(response.status_code, 201) def test_returns_201_for_unicode_chars(self): """Accept all valid characters.""" - url = reverse('bot:offtopicchannelname-list', host='api') + url = reverse('api:bot:offtopicchannelname-list') names = ( '𝖠𝖡𝖢𝖣𝖤𝖥𝖦𝖧𝖨𝖩𝖪𝖫𝖬𝖭𝖮𝖯𝖰𝖱𝖲𝖳𝖴𝖵𝖶𝖷𝖸𝖹', 'ǃ?’', @@ -123,7 +123,7 @@ class CreationTests(APISubdomainTestCase): def test_returns_400_for_missing_name_param(self): """Return error message when name not provided.""" - url = reverse('bot:offtopicchannelname-list', host='api') + url = reverse('api:bot:offtopicchannelname-list') response = self.client.post(url) self.assertEqual(response.status_code, 400) self.assertEqual(response.json(), { @@ -132,7 +132,7 @@ class CreationTests(APISubdomainTestCase): def test_returns_400_for_bad_name_param(self): """Return error message when invalid characters provided.""" - url = reverse('bot:offtopicchannelname-list', host='api') + url = reverse('api:bot:offtopicchannelname-list') invalid_names = ( 'space between words', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', @@ -147,7 +147,7 @@ class CreationTests(APISubdomainTestCase): }) -class DeletionTests(APISubdomainTestCase): +class DeletionTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand') @@ -155,25 +155,25 @@ class DeletionTests(APISubdomainTestCase): def test_deleting_unknown_name_returns_404(self): """Return 404 reponse when trying to delete unknown name.""" - url = reverse('bot:offtopicchannelname-detail', args=('unknown-name',), host='api') + url = reverse('api:bot:offtopicchannelname-detail', args=('unknown-name',)) response = self.client.delete(url) self.assertEqual(response.status_code, 404) def test_deleting_known_name_returns_204(self): """Return 204 response when deleting was successful.""" - url = reverse('bot:offtopicchannelname-detail', args=(self.test_name.name,), host='api') + url = reverse('api:bot:offtopicchannelname-detail', args=(self.test_name.name,)) response = self.client.delete(url) self.assertEqual(response.status_code, 204) def test_name_gets_deleted(self): """Name gets actually deleted.""" - url = reverse('bot:offtopicchannelname-detail', args=(self.test_name_2.name,), host='api') + url = reverse('api:bot:offtopicchannelname-detail', args=(self.test_name_2.name,)) response = self.client.delete(url) self.assertEqual(response.status_code, 204) - url = reverse('bot:offtopicchannelname-list', host='api') + url = reverse('api:bot:offtopicchannelname-list') response = self.client.get(url) self.assertNotIn(self.test_name_2.name, response.json()) diff --git a/pydis_site/apps/api/tests/test_offensive_message.py b/pydis_site/apps/api/tests/test_offensive_message.py index 0f3dbffa..9b79b38c 100644 --- a/pydis_site/apps/api/tests/test_offensive_message.py +++ b/pydis_site/apps/api/tests/test_offensive_message.py @@ -1,14 +1,14 @@ import datetime -from django_hosts.resolvers import reverse +from django.urls import reverse -from .base import APISubdomainTestCase +from .base import AuthenticatedAPITestCase from ..models import OffensiveMessage -class CreationTests(APISubdomainTestCase): +class CreationTests(AuthenticatedAPITestCase): def test_accept_valid_data(self): - url = reverse('bot:offensivemessage-list', host='api') + url = reverse('api:bot:offensivemessage-list') delete_at = datetime.datetime.now() + datetime.timedelta(days=1) data = { 'id': '602951077675139072', @@ -31,7 +31,7 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(data['channel_id'], str(offensive_message.channel_id)) def test_returns_400_on_non_future_date(self): - url = reverse('bot:offensivemessage-list', host='api') + url = reverse('api:bot:offensivemessage-list') delete_at = datetime.datetime.now() - datetime.timedelta(days=1) data = { 'id': '602951077675139072', @@ -45,7 +45,7 @@ class CreationTests(APISubdomainTestCase): }) def test_returns_400_on_negative_id_or_channel_id(self): - url = reverse('bot:offensivemessage-list', host='api') + url = reverse('api:bot:offensivemessage-list') delete_at = datetime.datetime.now() + datetime.timedelta(days=1) data = { 'id': '602951077675139072', @@ -69,7 +69,7 @@ class CreationTests(APISubdomainTestCase): }) -class ListTests(APISubdomainTestCase): +class ListTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): delete_at = datetime.datetime.now() + datetime.timedelta(days=1) @@ -100,7 +100,7 @@ class ListTests(APISubdomainTestCase): cls.messages[1]['delete_date'] = delete_at.isoformat() + 'Z' def test_get_data(self): - url = reverse('bot:offensivemessage-list', host='api') + url = reverse('api:bot:offensivemessage-list') response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -108,7 +108,7 @@ class ListTests(APISubdomainTestCase): self.assertEqual(response.json(), self.messages) -class DeletionTests(APISubdomainTestCase): +class DeletionTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): delete_at = datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(days=1) @@ -121,7 +121,7 @@ class DeletionTests(APISubdomainTestCase): def test_delete_data(self): url = reverse( - 'bot:offensivemessage-detail', host='api', args=(self.valid_offensive_message.id,) + 'api:bot:offensivemessage-detail', args=(self.valid_offensive_message.id,) ) response = self.client.delete(url) @@ -132,7 +132,7 @@ class DeletionTests(APISubdomainTestCase): ) -class NotAllowedMethodsTests(APISubdomainTestCase): +class NotAllowedMethodsTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): delete_at = datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(days=1) @@ -145,7 +145,7 @@ class NotAllowedMethodsTests(APISubdomainTestCase): def test_returns_405_for_patch_and_put_requests(self): url = reverse( - 'bot:offensivemessage-detail', host='api', args=(self.valid_offensive_message.id,) + 'api:bot:offensivemessage-detail', args=(self.valid_offensive_message.id,) ) not_allowed_methods = (self.client.patch, self.client.put) diff --git a/pydis_site/apps/api/tests/test_reminders.py b/pydis_site/apps/api/tests/test_reminders.py index 9dffb668..709685bc 100644 --- a/pydis_site/apps/api/tests/test_reminders.py +++ b/pydis_site/apps/api/tests/test_reminders.py @@ -1,52 +1,52 @@ from datetime import datetime from django.forms.models import model_to_dict -from django_hosts.resolvers import reverse +from django.urls import reverse -from .base import APISubdomainTestCase +from .base import AuthenticatedAPITestCase from ..models import Reminder, User -class UnauthedReminderAPITests(APISubdomainTestCase): +class UnauthedReminderAPITests(AuthenticatedAPITestCase): def setUp(self): super().setUp() self.client.force_authenticate(user=None) def test_list_returns_401(self): - url = reverse('bot:reminder-list', host='api') + url = reverse('api:bot:reminder-list') response = self.client.get(url) self.assertEqual(response.status_code, 401) def test_create_returns_401(self): - url = reverse('bot:reminder-list', host='api') + url = reverse('api:bot:reminder-list') response = self.client.post(url, data={'not': 'important'}) self.assertEqual(response.status_code, 401) def test_delete_returns_401(self): - url = reverse('bot:reminder-detail', args=('1234',), host='api') + url = reverse('api:bot:reminder-detail', args=('1234',)) response = self.client.delete(url) self.assertEqual(response.status_code, 401) -class EmptyDatabaseReminderAPITests(APISubdomainTestCase): +class EmptyDatabaseReminderAPITests(AuthenticatedAPITestCase): def test_list_all_returns_empty_list(self): - url = reverse('bot:reminder-list', host='api') + url = reverse('api:bot:reminder-list') response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), []) def test_delete_returns_404(self): - url = reverse('bot:reminder-detail', args=('1234',), host='api') + url = reverse('api:bot:reminder-detail', args=('1234',)) response = self.client.delete(url) self.assertEqual(response.status_code, 404) -class ReminderCreationTests(APISubdomainTestCase): +class ReminderCreationTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): cls.author = User.objects.create( @@ -64,7 +64,7 @@ class ReminderCreationTests(APISubdomainTestCase): 'channel_id': 123, 'mentions': [8888, 9999], } - url = reverse('bot:reminder-list', host='api') + url = reverse('api:bot:reminder-list') response = self.client.post(url, data=data) self.assertEqual(response.status_code, 201) self.assertIsNotNone(Reminder.objects.filter(id=1).first()) @@ -73,13 +73,13 @@ class ReminderCreationTests(APISubdomainTestCase): data = { 'author': self.author.id, # Missing multiple required fields } - url = reverse('bot:reminder-list', host='api') + url = reverse('api:bot:reminder-list') response = self.client.post(url, data=data) self.assertEqual(response.status_code, 400) self.assertRaises(Reminder.DoesNotExist, Reminder.objects.get, id=1) -class ReminderDeletionTests(APISubdomainTestCase): +class ReminderDeletionTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): cls.author = User.objects.create( @@ -97,20 +97,20 @@ class ReminderDeletionTests(APISubdomainTestCase): ) def test_delete_unknown_reminder_returns_404(self): - url = reverse('bot:reminder-detail', args=('something',), host='api') + url = reverse('api:bot:reminder-detail', args=('something',)) response = self.client.delete(url) self.assertEqual(response.status_code, 404) def test_delete_known_reminder_returns_204(self): - url = reverse('bot:reminder-detail', args=(self.reminder.id,), host='api') + url = reverse('api:bot:reminder-detail', args=(self.reminder.id,)) response = self.client.delete(url) self.assertEqual(response.status_code, 204) self.assertRaises(Reminder.DoesNotExist, Reminder.objects.get, id=self.reminder.id) -class ReminderListTests(APISubdomainTestCase): +class ReminderListTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): cls.author = User.objects.create( @@ -142,28 +142,28 @@ class ReminderListTests(APISubdomainTestCase): cls.rem_dict_two['expiration'] += 'Z' # Massaging a quirk of the response time format def test_reminders_in_full_list(self): - url = reverse('bot:reminder-list', host='api') + url = reverse('api:bot:reminder-list') response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertCountEqual(response.json(), [self.rem_dict_one, self.rem_dict_two]) def test_filter_search(self): - url = reverse('bot:reminder-list', host='api') + url = reverse('api:bot:reminder-list') response = self.client.get(f'{url}?search={self.author.name}') self.assertEqual(response.status_code, 200) self.assertCountEqual(response.json(), [self.rem_dict_one, self.rem_dict_two]) def test_filter_field(self): - url = reverse('bot:reminder-list', host='api') + url = reverse('api:bot:reminder-list') response = self.client.get(f'{url}?active=true') self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), [self.rem_dict_one]) -class ReminderRetrieveTests(APISubdomainTestCase): +class ReminderRetrieveTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): cls.author = User.objects.create( @@ -181,17 +181,17 @@ class ReminderRetrieveTests(APISubdomainTestCase): ) def test_retrieve_unknown_returns_404(self): - url = reverse('bot:reminder-detail', args=("not_an_id",), host='api') + url = reverse('api:bot:reminder-detail', args=("not_an_id",)) response = self.client.get(url) self.assertEqual(response.status_code, 404) def test_retrieve_known_returns_200(self): - url = reverse('bot:reminder-detail', args=(self.reminder.id,), host='api') + url = reverse('api:bot:reminder-detail', args=(self.reminder.id,)) response = self.client.get(url) self.assertEqual(response.status_code, 200) -class ReminderUpdateTests(APISubdomainTestCase): +class ReminderUpdateTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): cls.author = User.objects.create( @@ -211,7 +211,7 @@ class ReminderUpdateTests(APISubdomainTestCase): cls.data = {'content': 'Oops I forgot'} def test_patch_updates_record(self): - url = reverse('bot:reminder-detail', args=(self.reminder.id,), host='api') + url = reverse('api:bot:reminder-detail', args=(self.reminder.id,)) response = self.client.patch(url, data=self.data) self.assertEqual(response.status_code, 200) diff --git a/pydis_site/apps/api/tests/test_roles.py b/pydis_site/apps/api/tests/test_roles.py index 4d1a430c..d39cea4d 100644 --- a/pydis_site/apps/api/tests/test_roles.py +++ b/pydis_site/apps/api/tests/test_roles.py @@ -1,10 +1,10 @@ -from django_hosts.resolvers import reverse +from django.urls import reverse -from .base import APISubdomainTestCase +from .base import AuthenticatedAPITestCase from ..models import Role -class CreationTests(APISubdomainTestCase): +class CreationTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): cls.admins_role = Role.objects.create( @@ -78,7 +78,7 @@ class CreationTests(APISubdomainTestCase): def test_role_list(self): """Tests the GET list-view and validates the contents.""" - url = reverse('bot:role-list', host='api') + url = reverse('api:bot:role-list') response = self.client.get(url) self.assertContains(response, text="id", count=4, status_code=200) @@ -92,7 +92,7 @@ class CreationTests(APISubdomainTestCase): def test_role_get_detail_success(self): """Tests GET detail view of an existing role.""" - url = reverse('bot:role-detail', host='api', args=(self.admins_role.id, )) + url = reverse('api:bot:role-detail', args=(self.admins_role.id, )) response = self.client.get(url) self.assertContains(response, text="id", count=1, status_code=200) @@ -107,7 +107,7 @@ class CreationTests(APISubdomainTestCase): def test_role_post_201(self): """Tests creation of a role with a valid request.""" - url = reverse('bot:role-list', host='api') + url = reverse('api:bot:role-list') data = { "id": 1234567890, "name": "Role Creation Test", @@ -120,7 +120,7 @@ class CreationTests(APISubdomainTestCase): def test_role_post_invalid_request_body(self): """Tests creation of a role with an invalid request body.""" - url = reverse('bot:role-list', host='api') + url = reverse('api:bot:role-list') data = { "name": "Role Creation Test", "permissions": 0b01010010101, @@ -133,7 +133,7 @@ class CreationTests(APISubdomainTestCase): def test_role_put_200(self): """Tests PUT role request with valid request body.""" - url = reverse('bot:role-detail', host='api', args=(self.admins_role.id,)) + url = reverse('api:bot:role-detail', args=(self.admins_role.id,)) data = { "id": 123454321, "name": "Role Put Alteration Test", @@ -153,7 +153,7 @@ class CreationTests(APISubdomainTestCase): def test_role_put_invalid_request_body(self): """Tests PUT role request with invalid request body.""" - url = reverse('bot:role-detail', host='api', args=(self.admins_role.id,)) + url = reverse('api:bot:role-detail', args=(self.admins_role.id,)) data = { "name": "Role Put Alteration Test", "permissions": 255, @@ -165,7 +165,7 @@ class CreationTests(APISubdomainTestCase): def test_role_patch_200(self): """Tests PATCH role request with valid request body.""" - url = reverse('bot:role-detail', host='api', args=(self.admins_role.id,)) + url = reverse('api:bot:role-detail', args=(self.admins_role.id,)) data = { "name": "Owners" } @@ -177,13 +177,13 @@ class CreationTests(APISubdomainTestCase): def test_role_delete_200(self): """Tests DELETE requests for existing role.""" - url = reverse('bot:role-detail', host='api', args=(self.admins_role.id,)) + url = reverse('api:bot:role-detail', args=(self.admins_role.id,)) response = self.client.delete(url) self.assertEqual(response.status_code, 204) def test_role_detail_404_all_methods(self): """Tests detail view with non-existing ID.""" - url = reverse('bot:role-detail', host='api', args=(20190815,)) + url = reverse('api:bot:role-detail', args=(20190815,)) for method in ('get', 'put', 'patch', 'delete'): response = getattr(self.client, method)(url) diff --git a/pydis_site/apps/api/tests/test_rules.py b/pydis_site/apps/api/tests/test_rules.py index c94f89cc..d08c5fae 100644 --- a/pydis_site/apps/api/tests/test_rules.py +++ b/pydis_site/apps/api/tests/test_rules.py @@ -1,23 +1,23 @@ -from django_hosts.resolvers import reverse +from django.urls import reverse -from .base import APISubdomainTestCase +from .base import AuthenticatedAPITestCase from ..views import RulesView -class RuleAPITests(APISubdomainTestCase): +class RuleAPITests(AuthenticatedAPITestCase): def setUp(self): super().setUp() self.client.force_authenticate(user=None) def test_can_access_rules_view(self): - url = reverse('rules', host='api') + url = reverse('api:rules') response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertIsInstance(response.json(), list) def test_link_format_query_param_produces_different_results(self): - url = reverse('rules', host='api') + url = reverse('api:rules') markdown_links_response = self.client.get(url + '?link_format=md') html_links_response = self.client.get(url + '?link_format=html') self.assertNotEqual( @@ -30,6 +30,6 @@ class RuleAPITests(APISubdomainTestCase): RulesView._format_link("a", "b", "c") def test_get_returns_400_for_wrong_link_format(self): - url = reverse('rules', host='api') + url = reverse('api:rules') response = self.client.get(url + '?link_format=unknown') self.assertEqual(response.status_code, 400) diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py index c43b916a..bed6342e 100644 --- a/pydis_site/apps/api/tests/test_users.py +++ b/pydis_site/apps/api/tests/test_users.py @@ -1,44 +1,44 @@ from unittest.mock import patch from django.core.exceptions import ObjectDoesNotExist -from django_hosts.resolvers import reverse +from django.urls import reverse -from .base import APISubdomainTestCase +from .base import AuthenticatedAPITestCase from ..models import Role, User from ..models.bot.metricity import NotFound -class UnauthedUserAPITests(APISubdomainTestCase): +class UnauthedUserAPITests(AuthenticatedAPITestCase): def setUp(self): super().setUp() self.client.force_authenticate(user=None) def test_detail_lookup_returns_401(self): - url = reverse('bot:user-detail', args=('whatever',), host='api') + url = reverse('api:bot:user-detail', args=('whatever',)) response = self.client.get(url) self.assertEqual(response.status_code, 401) def test_list_returns_401(self): - url = reverse('bot:user-list', host='api') + url = reverse('api:bot:user-list') response = self.client.get(url) self.assertEqual(response.status_code, 401) def test_create_returns_401(self): - url = reverse('bot:user-list', host='api') + url = reverse('api:bot:user-list') response = self.client.post(url, data={'hi': 'there'}) self.assertEqual(response.status_code, 401) def test_delete_returns_401(self): - url = reverse('bot:user-detail', args=('whatever',), host='api') + url = reverse('api:bot:user-detail', args=('whatever',)) response = self.client.delete(url) self.assertEqual(response.status_code, 401) -class CreationTests(APISubdomainTestCase): +class CreationTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): cls.role = Role.objects.create( @@ -57,7 +57,7 @@ class CreationTests(APISubdomainTestCase): ) def test_accepts_valid_data(self): - url = reverse('bot:user-list', host='api') + url = reverse('api:bot:user-list') data = { 'id': 42, 'name': "Test", @@ -78,7 +78,7 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(user.in_guild, data['in_guild']) def test_supports_multi_creation(self): - url = reverse('bot:user-list', host='api') + url = reverse('api:bot:user-list') data = [ { 'id': 5, @@ -103,7 +103,7 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response.json(), []) def test_returns_400_for_unknown_role_id(self): - url = reverse('bot:user-list', host='api') + url = reverse('api:bot:user-list') data = { 'id': 5, 'name': "test man", @@ -117,7 +117,7 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response.status_code, 400) def test_returns_400_for_bad_data(self): - url = reverse('bot:user-list', host='api') + url = reverse('api:bot:user-list') data = { 'id': True, 'discriminator': "totally!" @@ -128,7 +128,7 @@ class CreationTests(APISubdomainTestCase): def test_returns_400_for_user_recreation(self): """Return 201 if User is already present in database as it skips User creation.""" - url = reverse('bot:user-list', host='api') + url = reverse('api:bot:user-list') data = [{ 'id': 11, 'name': 'You saw nothing.', @@ -140,7 +140,7 @@ class CreationTests(APISubdomainTestCase): def test_returns_400_for_duplicate_request_users(self): """Return 400 if 2 Users with same ID is passed in the request data.""" - url = reverse('bot:user-list', host='api') + url = reverse('api:bot:user-list') data = [ { 'id': 11, @@ -160,7 +160,7 @@ class CreationTests(APISubdomainTestCase): def test_returns_400_for_existing_user(self): """Returns 400 if user is already present in DB.""" - url = reverse('bot:user-list', host='api') + url = reverse('api:bot:user-list') data = { 'id': 11, 'name': 'You saw nothing part 3.', @@ -171,7 +171,7 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response.status_code, 400) -class MultiPatchTests(APISubdomainTestCase): +class MultiPatchTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): cls.role_developer = Role.objects.create( @@ -195,7 +195,7 @@ class MultiPatchTests(APISubdomainTestCase): ) def test_multiple_users_patch(self): - url = reverse("bot:user-bulk-patch", host="api") + url = reverse("api:bot:user-bulk-patch") data = [ { "id": 1, @@ -218,7 +218,7 @@ class MultiPatchTests(APISubdomainTestCase): self.assertEqual(user_2.name, data[1]["name"]) def test_returns_400_for_missing_user_id(self): - url = reverse("bot:user-bulk-patch", host="api") + url = reverse("api:bot:user-bulk-patch") data = [ { "name": "I am ghost user!", @@ -234,7 +234,7 @@ class MultiPatchTests(APISubdomainTestCase): self.assertEqual(response.status_code, 400) def test_returns_404_for_not_found_user(self): - url = reverse("bot:user-bulk-patch", host="api") + url = reverse("api:bot:user-bulk-patch") data = [ { "id": 1, @@ -252,7 +252,7 @@ class MultiPatchTests(APISubdomainTestCase): self.assertEqual(response.status_code, 404) def test_returns_400_for_bad_data(self): - url = reverse("bot:user-bulk-patch", host="api") + url = reverse("api:bot:user-bulk-patch") data = [ { "id": 1, @@ -268,7 +268,7 @@ class MultiPatchTests(APISubdomainTestCase): self.assertEqual(response.status_code, 400) def test_returns_400_for_insufficient_data(self): - url = reverse("bot:user-bulk-patch", host="api") + url = reverse("api:bot:user-bulk-patch") data = [ { "id": 1, @@ -282,7 +282,7 @@ class MultiPatchTests(APISubdomainTestCase): def test_returns_400_for_duplicate_request_users(self): """Return 400 if 2 Users with same ID is passed in the request data.""" - url = reverse("bot:user-bulk-patch", host="api") + url = reverse("api:bot:user-bulk-patch") data = [ { 'id': 1, @@ -297,7 +297,7 @@ class MultiPatchTests(APISubdomainTestCase): self.assertEqual(response.status_code, 400) -class UserModelTests(APISubdomainTestCase): +class UserModelTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): cls.role_top = Role.objects.create( @@ -353,7 +353,7 @@ class UserModelTests(APISubdomainTestCase): self.assertEqual(self.user_with_roles.username, "Test User with two roles#0001") -class UserPaginatorTests(APISubdomainTestCase): +class UserPaginatorTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): users = [] @@ -367,7 +367,7 @@ class UserPaginatorTests(APISubdomainTestCase): cls.users = User.objects.bulk_create(users) def test_returns_single_page_response(self): - url = reverse("bot:user-list", host="api") + url = reverse("api:bot:user-list") response = self.client.get(url).json() self.assertIsNone(response["next_page_no"]) self.assertIsNone(response["previous_page_no"]) @@ -379,7 +379,7 @@ class UserPaginatorTests(APISubdomainTestCase): discriminator=1111, in_guild=True ) - url = reverse("bot:user-list", host="api") + url = reverse("api:bot:user-list") response = self.client.get(url).json() self.assertEqual(2, response["next_page_no"]) @@ -390,12 +390,12 @@ class UserPaginatorTests(APISubdomainTestCase): discriminator=1111, in_guild=True ) - url = reverse("bot:user-list", host="api") + url = reverse("api:bot:user-list") response = self.client.get(url, {"page": 2}).json() self.assertEqual(1, response["previous_page_no"]) -class UserMetricityTests(APISubdomainTestCase): +class UserMetricityTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): User.objects.create( @@ -413,7 +413,7 @@ class UserMetricityTests(APISubdomainTestCase): self.mock_metricity_user(joined_at, total_messages, total_blocks, []) # When - url = reverse('bot:user-metricity-data', args=[0], host='api') + url = reverse('api:bot:user-metricity-data', args=[0]) response = self.client.get(url) # Then @@ -430,7 +430,7 @@ class UserMetricityTests(APISubdomainTestCase): self.mock_no_metricity_user() # When - url = reverse('bot:user-metricity-data', args=[0], host='api') + url = reverse('api:bot:user-metricity-data', args=[0]) response = self.client.get(url) # Then @@ -441,7 +441,7 @@ class UserMetricityTests(APISubdomainTestCase): self.mock_no_metricity_user() # When - url = reverse('bot:user-metricity-review-data', args=[0], host='api') + url = reverse('api:bot:user-metricity-review-data', args=[0]) response = self.client.get(url) # Then @@ -460,7 +460,7 @@ class UserMetricityTests(APISubdomainTestCase): with patch("pydis_site.apps.api.viewsets.bot.user.Infraction.objects.get") as p: p.side_effect = case['exception'] - url = reverse('bot:user-metricity-data', args=[0], host='api') + url = reverse('api:bot:user-metricity-data', args=[0]) response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -475,7 +475,7 @@ class UserMetricityTests(APISubdomainTestCase): self.mock_metricity_user(joined_at, total_messages, total_blocks, channel_activity) # When - url = reverse('bot:user-metricity-review-data', args=[0], host='api') + url = reverse('api:bot:user-metricity-review-data', args=[0]) response = self.client.get(url) # Then 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 826ad25e..922e6555 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 @@ -20,7 +20,7 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): ### GET /bot/off-topic-channel-names Return all known off-topic channel names from the database. If the `random_items` query parameter is given, for example using... - $ curl api.pythondiscord.local:8000/bot/off-topic-channel-names?random_items=5 + $ curl 127.0.0.1:8000/api/bot/off-topic-channel-names?random_items=5 ... then the API will return `5` random items from the database that is not used in current rotation. When running out of names, API will mark all names to not used and start new rotation. @@ -39,7 +39,7 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): ### POST /bot/off-topic-channel-names Create a new off-topic-channel name in the database. The name must be given as a query parameter, for example: - $ curl api.pythondiscord.local:8000/bot/off-topic-channel-names?name=lemons-lemonade-shop + $ curl 127.0.0.1:8000/api/bot/off-topic-channel-names?name=lemons-lemonade-shop #### Status codes - 201: returned on success 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 741bf28e..7419858e 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 @@ -43,7 +43,6 @@ For any staff member, since you have write permissions already to the original r # Development environment 1. [Clone your fork to a local project directory](../cloning-repository/) 2. [Install the project's dependencies](../installing-project-dependencies/) -3. [Prepare your hosts file (Optional)](../hosts-file/) --- # Test server and bot account diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/hosts-file.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/hosts-file.md index 5d55a7f3..bba5722d 100644 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/hosts-file.md +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/hosts-file.md @@ -8,16 +8,13 @@ toc: 3 # What's a hosts file? The hosts file maps a hostname/domain to an IP address, allowing you to visit a given domain on your browser and have it resolve by your system to the given IP address, even if it's pointed back to your own system or network. -When staging a local [Site](https://pythondiscord.com/pages/contributing/site/) project, you will need to add some entries to your hosts file so you can visit the site with the domain `http://pythondiscord.local` +When staging a local [Site](https://pythondiscord.com/pages/contributing/site/) project, you may want to add an entries to your hosts file so you can visit the site with the domain `http://pythondiscord.local`. This is purely for convenience, and you can use `localhost` or `127.0.0.1` instead if you prefer. # What to add -You would add the following entries to your hosts file. +You would add the following entry to your hosts file. ```plaintext 127.0.0.1 pythondiscord.local -127.0.0.1 api.pythondiscord.local -127.0.0.1 staff.pythondiscord.local -127.0.0.1 admin.pythondiscord.local ``` # How to add it diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/site.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/site.md index ada47931..c4f29887 100644 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/site.md +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/site.md @@ -43,7 +43,6 @@ For any Core Developers, since you have write permissions already to the origina 1. [Clone your fork to a local project directory](../cloning-repository/) 2. [Install the project's dependencies](../installing-project-dependencies/) -3. [Prepare your hosts file](../hosts-file/) ## Without Docker @@ -81,7 +80,8 @@ STATIC_ROOT=staticfiles #### Notes regarding `DATABASE_URL` -- If the database is hosted locally i.e. on the same machine as the webserver, then use `localhost` for the host. Windows and macOS users may need to use the [Docker host IP](../hosts-file/#windows) instead. +- If the database is hosted locally i.e. on the same machine as the webserver, then use `localhost` for the host. Windows and macOS users may + need to use the [Docker host IP](https://stackoverflow.com/questions/22944631/how-to-get-the-ip-address-of-the-docker-host-from-inside-a-docker-container) instead. - If the database is running in Docker, use port `7777`. Otherwise, use `5432` as that is the default port used by PostegreSQL. - If you configured PostgreSQL in a different manner or you are not hosting it locally, then you will need to determine the correct host and port yourself. The user, password, and database name should all still be `pysite` unless you deviated from the setup instructions in the previous section. @@ -142,4 +142,4 @@ Unless you are editing the Dockerfile or docker-compose.yml, you shouldn't need Django provides an interface for administration with which you can view and edit the models among other things. -It can be found at [http://admin.pythondiscord.local:8000](http://admin.pythondiscord.local:8000). The default credentials are `admin` for the username and `admin` for the password. +It can be found at [http://127.0.0.1:8000/admin/](http://127.0.0.1:8000/admin/). The default credentials are `admin` for the username and `admin` for the password. diff --git a/pydis_site/apps/events/tests/test_views.py b/pydis_site/apps/events/tests/test_views.py index 23c9e596..669fbf82 100644 --- a/pydis_site/apps/events/tests/test_views.py +++ b/pydis_site/apps/events/tests/test_views.py @@ -2,7 +2,7 @@ from pathlib import Path from django.conf import settings from django.test import TestCase, override_settings -from django_hosts.resolvers import reverse +from django.urls import reverse PAGES_PATH = Path(settings.BASE_DIR, "pydis_site", "templates", "events", "test-pages") @@ -21,8 +21,8 @@ class PageTests(TestCase): def test_valid_event_page_reponse_200(self): """Should return response code 200 when visiting valid event page.""" pages = ( - reverse("events:page", ("my-event",)), - reverse("events:page", ("my-event/subpage",)), + reverse("events:page", args=("my-event",)), + reverse("events:page", args=("my-event/subpage",)), ) for page in pages: with self.subTest(page=page): @@ -33,8 +33,8 @@ class PageTests(TestCase): def test_invalid_event_page_404(self): """Should return response code 404 when visiting invalid event page.""" pages = ( - reverse("events:page", ("invalid",)), - reverse("events:page", ("invalid/invalid",)) + reverse("events:page", args=("invalid",)), + reverse("events:page", args=("invalid/invalid",)) ) for page in pages: with self.subTest(page=page): diff --git a/pydis_site/apps/home/tests/test_views.py b/pydis_site/apps/home/tests/test_views.py index bd1671b1..b1215df4 100644 --- a/pydis_site/apps/home/tests/test_views.py +++ b/pydis_site/apps/home/tests/test_views.py @@ -1,10 +1,10 @@ from django.test import TestCase -from django_hosts.resolvers import reverse +from django.urls import reverse class TestIndexReturns200(TestCase): def test_index_returns_200(self): """Check that the index page returns a HTTP 200 response.""" - url = reverse('home') + url = reverse('home:home') resp = self.client.get(url) self.assertEqual(resp.status_code, 200) diff --git a/pydis_site/apps/home/urls.py b/pydis_site/apps/home/urls.py index 1e2af8f3..57abc942 100644 --- a/pydis_site/apps/home/urls.py +++ b/pydis_site/apps/home/urls.py @@ -1,15 +1,9 @@ -from django.contrib import admin -from django.urls import include, path +from django.urls import path from .views import HomeView, timeline app_name = 'home' urlpatterns = [ path('', HomeView.as_view(), name='home'), - path('', include('pydis_site.apps.redirect.urls')), - path('admin/', admin.site.urls), - path('resources/', include('pydis_site.apps.resources.urls')), - path('pages/', include('pydis_site.apps.content.urls')), - path('events/', include('pydis_site.apps.events.urls', namespace='events')), path('timeline/', timeline, name="timeline"), ] diff --git a/pydis_site/apps/redirect/tests.py b/pydis_site/apps/redirect/tests.py index 2cfa3478..c181d6e5 100644 --- a/pydis_site/apps/redirect/tests.py +++ b/pydis_site/apps/redirect/tests.py @@ -31,7 +31,7 @@ class RedirectTests(TestCase): ): resp = self.client.get( reverse( - f"home:redirect:{name}", + f"redirect:{name}", args=TESTING_ARGUMENTS.get(name, ()) ), follow=True @@ -53,7 +53,7 @@ class RedirectTests(TestCase): self.assertRedirects( resp, reverse( - f"home:{data['redirect_route']}", + f"{data['redirect_route']}", args=expected_args ), status_code=301 diff --git a/pydis_site/apps/resources/tests/test_views.py b/pydis_site/apps/resources/tests/test_views.py index 53685eef..3ad0b958 100644 --- a/pydis_site/apps/resources/tests/test_views.py +++ b/pydis_site/apps/resources/tests/test_views.py @@ -3,7 +3,7 @@ from unittest.mock import patch from django.conf import settings from django.test import TestCase -from django_hosts import reverse +from django.urls import reverse TESTING_RESOURCES_PATH = Path( settings.BASE_DIR, "pydis_site", "apps", "resources", "tests", "testing_resources" @@ -22,13 +22,13 @@ 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", ("testing",)) + url = reverse("resources:resources", args=("testing",)) 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", ("invalid",)) + url = reverse("resources:resources", args=("invalid",)) response = self.client.get(url) self.assertEqual(response.status_code, 404) diff --git a/pydis_site/apps/staff/tests/test_logs_view.py b/pydis_site/apps/staff/tests/test_logs_view.py index 00e0ab2f..45e9ce8f 100644 --- a/pydis_site/apps/staff/tests/test_logs_view.py +++ b/pydis_site/apps/staff/tests/test_logs_view.py @@ -1,6 +1,6 @@ -from django.test import Client, TestCase +from django.test import TestCase +from django.urls import reverse from django.utils import timezone -from django_hosts.resolvers import reverse, reverse_host from pydis_site.apps.api.models.bot import DeletedMessage, MessageDeletionContext, Role, User from pydis_site.apps.staff.templatetags.deletedmessage_filters import hex_colour @@ -105,22 +105,18 @@ class TestLogsView(TestCase): deletion_context=cls.deletion_context, ) - def setUp(self): - """Sets up a test client that automatically sets the correct HOST header.""" - self.client = Client(HTTP_HOST=reverse_host(host="staff")) - def test_logs_returns_200_for_existing_logs_pk(self): - url = reverse('logs', host="staff", args=(self.deletion_context.id,)) + url = reverse('staff:logs', args=(self.deletion_context.id,)) response = self.client.get(url) self.assertEqual(response.status_code, 200) def test_logs_returns_404_for_nonexisting_logs_pk(self): - url = reverse('logs', host="staff", args=(self.deletion_context.id + 100,)) + url = reverse('staff:logs', args=(self.deletion_context.id + 100,)) response = self.client.get(url) self.assertEqual(response.status_code, 404) def test_author_color_is_set_in_response(self): - url = reverse('logs', host="staff", args=(self.deletion_context.id,)) + url = reverse('staff:logs', args=(self.deletion_context.id,)) response = self.client.get(url) role_colour = hex_colour(self.developers_role.colour) html_needle = ( @@ -129,7 +125,7 @@ class TestLogsView(TestCase): self.assertInHTML(html_needle, response.content.decode()) def test_correct_messages_have_been_passed_to_template(self): - url = reverse('logs', host="staff", args=(self.deletion_context.id,)) + url = reverse('staff:logs', args=(self.deletion_context.id,)) response = self.client.get(url) self.assertIn("messages", response.context) self.assertListEqual( @@ -138,7 +134,7 @@ class TestLogsView(TestCase): ) def test_if_both_embeds_are_included_html_response(self): - url = reverse('logs', host="staff", args=(self.deletion_context.id,)) + url = reverse('staff:logs', args=(self.deletion_context.id,)) response = self.client.get(url) html_response = response.content.decode() @@ -151,7 +147,7 @@ class TestLogsView(TestCase): self.assertInHTML(embed_colour_needle.format(colour=embed_two_colour), html_response) def test_if_both_attachments_are_included_html_response(self): - url = reverse('logs', host="staff", args=(self.deletion_context.id,)) + url = reverse('staff:logs', args=(self.deletion_context.id,)) response = self.client.get(url) html_response = response.content.decode() @@ -166,7 +162,7 @@ class TestLogsView(TestCase): ) def test_if_html_in_content_is_properly_escaped(self): - url = reverse('logs', host="staff", args=(self.deletion_context.id,)) + url = reverse('staff:logs', args=(self.deletion_context.id,)) response = self.client.get(url) html_response = response.content.decode() diff --git a/pydis_site/hosts.py b/pydis_site/hosts.py deleted file mode 100644 index 5a837a8b..00000000 --- a/pydis_site/hosts.py +++ /dev/null @@ -1,13 +0,0 @@ -from django.conf import settings -from django_hosts import host, patterns - -host_patterns = patterns( - '', - host(r'admin', 'pydis_site.apps.admin.urls', name="admin"), - # External API ingress (over the net) - host(r'api', 'pydis_site.apps.api.urls', name='api'), - # Internal API ingress (cluster local) - host(r'pydis-api', 'pydis_site.apps.api.urls', name='internal_api'), - host(r'staff', 'pydis_site.apps.staff.urls', name='staff'), - host(r'.*', 'pydis_site.apps.home.urls', name=settings.DEFAULT_HOST) -) diff --git a/pydis_site/settings.py b/pydis_site/settings.py index 7df7ad85..f7c4401d 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -52,13 +52,7 @@ elif 'CI' in os.environ: else: ALLOWED_HOSTS = env.list( 'ALLOWED_HOSTS', - default=[ - 'pythondiscord.com', - 'admin.pythondiscord.com', - 'api.pythondiscord.com', - 'staff.pythondiscord.com', - 'pydis-api.default.svc.cluster.local', - ] + default=['pythondiscord.com'], ) SECRET_KEY = env('SECRET_KEY') @@ -80,7 +74,6 @@ INSTALLED_APPS = [ 'django.contrib.sites', 'django.contrib.staticfiles', - 'django_hosts', 'django_filters', 'django_simple_bulma', 'rest_framework', @@ -88,8 +81,6 @@ INSTALLED_APPS = [ ] MIDDLEWARE = [ - 'django_hosts.middleware.HostsRequestMiddleware', - 'django.middleware.security.SecurityMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -98,8 +89,6 @@ MIDDLEWARE = [ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', - - 'django_hosts.middleware.HostsResponseMiddleware', ] ROOT_URLCONF = 'pydis_site.urls' @@ -109,10 +98,6 @@ TEMPLATES = [ 'DIRS': [os.path.join(BASE_DIR, 'pydis_site', 'templates')], 'APP_DIRS': True, 'OPTIONS': { - 'builtins': [ - 'django_hosts.templatetags.hosts_override', - ], - 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', @@ -174,11 +159,6 @@ STATICFILES_FINDERS = [ 'django_simple_bulma.finders.SimpleBulmaFinder', ] -# django-hosts -# https://django-hosts.readthedocs.io/en/latest/ -ROOT_HOSTCONF = 'pydis_site.hosts' -DEFAULT_HOST = 'home' - if DEBUG: PARENT_HOST = env('PARENT_HOST', default='pythondiscord.local:8000') diff --git a/pydis_site/templates/base/navbar.html b/pydis_site/templates/base/navbar.html index 4cc49dc6..7d5119c7 100644 --- a/pydis_site/templates/base/navbar.html +++ b/pydis_site/templates/base/navbar.html @@ -76,7 +76,7 @@ FAQ - + Timeline diff --git a/pydis_site/templates/home/index.html b/pydis_site/templates/home/index.html index 18f6b77b..cbede038 100644 --- a/pydis_site/templates/home/index.html +++ b/pydis_site/templates/home/index.html @@ -17,7 +17,7 @@
Thanks to all our members for helping us create this friendly and helpful community!

- As a nice treat, we've created a
Timeline page for people + As a nice treat, we've created a Timeline page for people to discover the events that made our community what it is today. Be sure to check it out!
@@ -108,7 +108,7 @@

- + Check it out! diff --git a/pydis_site/urls.py b/pydis_site/urls.py index 47cf0ba1..3c9fe347 100644 --- a/pydis_site/urls.py +++ b/pydis_site/urls.py @@ -1,7 +1,21 @@ +from django.contrib import admin from django.urls import include, path urlpatterns = ( - path('', include('pydis_site.apps.home.urls', namespace='home')), + path('admin/', admin.site.urls), + + # External API ingress (over the net) + path('api/', include('pydis_site.apps.api.urls', namespace='api')), + # 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('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('staff/', include('pydis_site.apps.staff.urls', namespace='staff')), + path('', include('pydis_site.apps.home.urls', namespace='home')), ) diff --git a/pyproject.toml b/pyproject.toml index 5bbf86ee..0b546781 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,6 @@ python = "3.9.*" django = "~=3.0.4" django-environ = "~=0.4.5" django-filter = "~=2.1.0" -django-hosts = "~=4.0" djangorestframework = "~=3.11.0" psycopg2-binary = "~=2.8" django-simple-bulma = "~=2.1" -- cgit v1.2.3 From da1056f36f77d98783b8fb53152fd4ebbc19c019 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Thu, 10 Jun 2021 02:08:54 +0530 Subject: Lint file and remove update() method declaration from OffTopicChannelNameListSerializer. --- pydis_site/apps/api/serializers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 1acb4fb8..0d505675 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -203,12 +203,12 @@ class ExpandedInfractionSerializer(InfractionSerializer): class OffTopicChannelNameListSerializer(ListSerializer): - def update(self, instance, validated_data): - pass + """Custom ListSerializer to override to_representation() when list views are triggered.""" def to_representation(self, objects: List[OffTopicChannelName]) -> List[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 @@ -222,6 +222,7 @@ class OffTopicChannelNameSerializer(ModelSerializer): class Meta: """Metadata defined for the Django REST Framework.""" + list_serializer_class = OffTopicChannelNameListSerializer model = OffTopicChannelName fields = ('name', 'used', 'active') -- cgit v1.2.3 From f0534b8e1ed55c18416372f23bb5a1f2e0862e38 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Thu, 10 Jun 2021 02:09:22 +0530 Subject: Fix bug: Do not force active param. --- .../apps/api/viewsets/bot/off_topic_channel_name.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) 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 0caf8088..18ee84ea 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,11 +1,9 @@ -import json - from django.db.models import Case, Value, When from django.db.models.query import QuerySet from django.shortcuts import get_object_or_404 from rest_framework.exceptions import ParseError -from rest_framework.response import Response 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 ModelViewSet @@ -70,9 +68,9 @@ class OffTopicChannelNameViewSet(ModelViewSet): name = self.kwargs[self.lookup_field] return get_object_or_404(queryset, name=name) - def get_queryset(self, active=True) -> QuerySet: + def get_queryset(self, **kwargs) -> QuerySet: """Returns a queryset that covers the entire OffTopicChannelName table.""" - return OffTopicChannelName.objects.filter(active=True) + return OffTopicChannelName.objects.filter(**kwargs) def create(self, request: Request, *args, **kwargs) -> Response: """ @@ -134,6 +132,11 @@ class OffTopicChannelNameViewSet(ModelViewSet): serialized = self.serializer_class(queryset, many=True) return Response(serialized.data) - queryset = self.get_queryset(active=bool(request.query_params.get("active", True))) + params = {} + + if active_param := request.query_params.get("active"): + params["active"] = active_param.lower() == "true" + + queryset = self.get_queryset(**params) serialized = self.serializer_class(queryset, many=True) return Response(serialized.data) -- cgit v1.2.3 From 832880cfac4206aaba0e7de8f005c6425da7a8f3 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Thu, 10 Jun 2021 02:11:40 +0530 Subject: Add tests for active params. --- .../apps/api/tests/test_off_topic_channel_names.py | 38 +++++++++++++++++++--- 1 file changed, 34 insertions(+), 4 deletions(-) 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 3ab8b22d..a407654c 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,15 @@ class EmptyDatabaseTests(APISubdomainTestCase): class ListTests(APISubdomainTestCase): @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=True, active=True + ) + cls.test_name_3 = OffTopicChannelName.objects.create( + name="frozen-with-iceman", used=True, active=False + ) def test_returns_name_in_list(self): """Return all off-topic channel names.""" @@ -78,7 +85,8 @@ class ListTests(APISubdomainTestCase): response.json(), [ self.test_name.name, - self.test_name_2.name + self.test_name_2.name, + self.test_name_3.name ] ) @@ -97,7 +105,29 @@ class ListTests(APISubdomainTestCase): response = self.client.get(f'{url}?random_items=2') self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), [self.test_name.name, self.test_name_2.name]) + self.assertEqual(response.json(), [self.test_name.name, self.test_name_3.name]) + + def test_returns_inactive_ot_names(self): + """Return inactive off topic names.""" + url = reverse('bot:offtopicchannelname-list', host="api") + 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('bot:offtopicchannelname-list', host="api") + response = self.client.get(f"{url}?active=true") + + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json(), + [self.test_name.name, self.test_name_2.name] + ) class CreationTests(APISubdomainTestCase): -- cgit v1.2.3 From 3795b6d6de005f0ed00c37cf042eaca01d0a4769 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Thu, 10 Jun 2021 02:32:17 +0530 Subject: Use assertListEqual where applicable instead of assertEqual. --- .../apps/api/tests/test_off_topic_channel_names.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) 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 a407654c..ebb1224a 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 @@ -69,7 +69,7 @@ class ListTests(APISubdomainTestCase): name='lemons-lemonade-stand', used=False, active=True ) cls.test_name_2 = OffTopicChannelName.objects.create( - name='bbq-with-bisk', used=True, active=True + name='bbq-with-bisk', used=False, active=True ) cls.test_name_3 = OffTopicChannelName.objects.create( name="frozen-with-iceman", used=True, active=False @@ -81,7 +81,7 @@ class ListTests(APISubdomainTestCase): response = self.client.get(url) self.assertEqual(response.status_code, 200) - self.assertEqual( + self.assertListEqual( response.json(), [ self.test_name.name, @@ -90,22 +90,24 @@ class ListTests(APISubdomainTestCase): ] ) - def test_returns_single_item_with_random_items_param_set_to_1(self): + def test_returns_two_items_with_random_items_param_set_to_2(self): """Return not-used name instead used.""" url = reverse('bot:offtopicchannelname-list', host='api') - 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.assertEqual(response.json(), [self.test_name.name, self.test_name_2.name]) def test_running_out_of_names_with_random_parameter(self): """Reset names `used` parameter to `False` when running out of names.""" url = reverse('bot:offtopicchannelname-list', host='api') - 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_3.name]) + self.assertListEqual( + response.json(), [self.test_name.name, self.test_name_2.name, self.test_name_3.name] + ) def test_returns_inactive_ot_names(self): """Return inactive off topic names.""" -- cgit v1.2.3 From 8fdabfb4c08931b1e2352e98b307b3bfa3a121f1 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Thu, 10 Jun 2021 02:45:55 +0530 Subject: Use sets to compare 2 un-ordered lists. --- .../apps/api/tests/test_off_topic_channel_names.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) 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 ebb1224a..34dde7c6 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 @@ -81,13 +81,13 @@ class ListTests(APISubdomainTestCase): response = self.client.get(url) self.assertEqual(response.status_code, 200) - self.assertListEqual( - response.json(), - [ + self.assertEqual( + set(response.json()), + { self.test_name.name, self.test_name_2.name, self.test_name_3.name - ] + } ) def test_returns_two_items_with_random_items_param_set_to_2(self): @@ -97,7 +97,7 @@ class ListTests(APISubdomainTestCase): self.assertEqual(response.status_code, 200) self.assertEqual(len(response.json()), 2) - 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}) def test_running_out_of_names_with_random_parameter(self): """Reset names `used` parameter to `False` when running out of names.""" @@ -105,8 +105,9 @@ class ListTests(APISubdomainTestCase): response = self.client.get(f'{url}?random_items=3') self.assertEqual(response.status_code, 200) - self.assertListEqual( - response.json(), [self.test_name.name, self.test_name_2.name, self.test_name_3.name] + self.assertEqual( + set(response.json()), + {self.test_name.name, self.test_name_2.name, self.test_name_3.name} ) def test_returns_inactive_ot_names(self): @@ -127,8 +128,8 @@ class ListTests(APISubdomainTestCase): self.assertEqual(response.status_code, 200) self.assertEqual( - response.json(), - [self.test_name.name, self.test_name_2.name] + set(response.json()), + {self.test_name.name, self.test_name_2.name} ) -- cgit v1.2.3 From e7b4da7955c100d42536e298ffc9966dd9f95c48 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Thu, 22 Jul 2021 13:51:28 +0530 Subject: Update docstring. --- pydis_site/apps/api/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 0d505675..957c85f3 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -207,7 +207,7 @@ class OffTopicChannelNameListSerializer(ListSerializer): def to_representation(self, objects: List[OffTopicChannelName]) -> List[str]: """ - Return the representation of this `OffTopicChannelName`. + Return a list representing a list of `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. -- cgit v1.2.3 From d52f21c1fefbb396762dd74609b9957ae029dfc3 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Sat, 24 Jul 2021 19:26:36 +0530 Subject: fix CommandError: Conflicting migrations detected. --- pydis_site/apps/api/migrations/0072_merge_20210724_1354.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0072_merge_20210724_1354.py 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 = [ + ] -- cgit v1.2.3 From a2494d4275c6bbe578156601b0fda0cf203e7af7 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Thu, 9 Sep 2021 15:53:32 +0100 Subject: Init metricity using docker-compose init volume --- docker-compose.yml | 2 ++ manage.py | 37 --------------------------- postgres/init.sql | 73 +++++++++++++++++++----------------------------------- 3 files changed, 28 insertions(+), 84 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 37678949..05867a46 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,6 +18,8 @@ services: POSTGRES_DB: pysite POSTGRES_PASSWORD: pysite POSTGRES_USER: pysite + volumes: + - ./postgres/init.sql:/docker-entrypoint-initdb.d/init.sql web: build: diff --git a/manage.py b/manage.py index 648d6635..d8258281 100755 --- a/manage.py +++ b/manage.py @@ -141,44 +141,8 @@ class SiteManager: name="pythondiscord.local:8000" ) - @staticmethod - def run_metricity_init() -> None: - """ - Initialise metricity relations and populate with some testing data. - - This is done at run time since other projects, like Python bot, - rely on the site initialising it's own db, since they do not have - access to the init.sql file to mount a docker-compose volume. - """ - import psycopg2 - from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT - - print("Initialising metricity.") - - db_url_parts = SiteManager.parse_db_url(os.environ["DATABASE_URL"]) - conn = psycopg2.connect( - host=db_url_parts.hostname, - port=db_url_parts.port, - user=db_url_parts.username, - password=db_url_parts.password, - database=db_url_parts.path[1:] - ) - # Required to create a db from `cursor.execute()` - conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) - - with conn.cursor() as cursor, open("postgres/init.sql", encoding="utf-8") as f: - cursor.execute( - f.read(), - ("metricity", db_url_parts.username, db_url_parts.password) - ) - conn.close() - def prepare_server(self) -> None: """Perform preparation tasks before running the server.""" - self.wait_for_postgres() - if self.debug: - self.run_metricity_init() - django.setup() print("Applying migrations.") @@ -236,7 +200,6 @@ def main() -> None: # Always run metricity init when in CI, indicated by the CI env var if os.environ.get("CI", "false").lower() == "true": SiteManager.wait_for_postgres() - SiteManager.run_metricity_init() # Use the custom site manager for launching the server if len(sys.argv) > 1 and sys.argv[1] == "run": diff --git a/postgres/init.sql b/postgres/init.sql index 55bb468f..190a705c 100644 --- a/postgres/init.sql +++ b/postgres/init.sql @@ -1,29 +1,8 @@ --- The following function is from Stack Overflow --- https://stackoverflow.com/questions/18389124/simulate-create-database-if-not-exists-for-postgresql/36218838#36218838 --- User frankhommers (https://stackoverflow.com/users/971229/frankhommers) - -DO -$do$ -DECLARE - _db TEXT := %s; - _user TEXT := %s; - _password TEXT := %s; -BEGIN - CREATE EXTENSION IF NOT EXISTS dblink; - IF EXISTS (SELECT 1 FROM pg_database WHERE datname = _db) THEN - RAISE NOTICE 'Database already exists'; - ELSE - PERFORM dblink_connect( - 'host=localhost user=' || _user || - ' password=' || _password || - ' dbname=' || current_database() - ); - PERFORM dblink_exec('CREATE DATABASE ' || _db); - END IF; -END -$do$; - -CREATE TABLE IF NOT EXISTS users ( +CREATE DATABASE metricity; + +\c metricity; + +CREATE TABLE users ( id varchar, joined_at timestamp, primary key(id) @@ -32,14 +11,14 @@ CREATE TABLE IF NOT EXISTS users ( INSERT INTO users VALUES ( 0, current_timestamp -) ON CONFLICT (id) DO NOTHING; +); INSERT INTO users VALUES ( 1, current_timestamp -) ON CONFLICT (id) DO NOTHING; +); -CREATE TABLE IF NOT EXISTS channels ( +CREATE TABLE channels ( id varchar, name varchar, primary key(id) @@ -48,44 +27,44 @@ CREATE TABLE IF NOT EXISTS channels ( INSERT INTO channels VALUES( '267659945086812160', 'python-general' -) ON CONFLICT (id) DO NOTHING; +); INSERT INTO channels VALUES( '11', 'help-apple' -) ON CONFLICT (id) DO NOTHING; +); INSERT INTO channels VALUES( '12', 'help-cherry' -) ON CONFLICT (id) DO NOTHING; +); INSERT INTO channels VALUES( '21', 'ot0-hello' -) ON CONFLICT (id) DO NOTHING; +); INSERT INTO channels VALUES( '22', 'ot1-world' -) ON CONFLICT (id) DO NOTHING; +); INSERT INTO channels VALUES( '31', 'voice-chat-0' -) ON CONFLICT (id) DO NOTHING; +); INSERT INTO channels VALUES( '32', 'code-help-voice-0' -) ON CONFLICT (id) DO NOTHING; +); INSERT INTO channels VALUES( '1234', 'zebra' -) ON CONFLICT (id) DO NOTHING; +); -CREATE TABLE IF NOT EXISTS messages ( +CREATE TABLE messages ( id varchar, author_id varchar references users(id), is_deleted boolean, @@ -100,7 +79,7 @@ INSERT INTO messages VALUES( false, now(), '267659945086812160' -) ON CONFLICT (id) DO NOTHING; +); INSERT INTO messages VALUES( 1, @@ -108,7 +87,7 @@ INSERT INTO messages VALUES( false, now() + INTERVAL '10 minutes,', '1234' -) ON CONFLICT (id) DO NOTHING; +); INSERT INTO messages VALUES( 2, @@ -116,7 +95,7 @@ INSERT INTO messages VALUES( false, now(), '11' -) ON CONFLICT (id) DO NOTHING; +); INSERT INTO messages VALUES( 3, @@ -124,7 +103,7 @@ INSERT INTO messages VALUES( false, now(), '12' -) ON CONFLICT (id) DO NOTHING; +); INSERT INTO messages VALUES( 4, @@ -132,7 +111,7 @@ INSERT INTO messages VALUES( false, now(), '21' -) ON CONFLICT (id) DO NOTHING; +); INSERT INTO messages VALUES( 5, @@ -140,7 +119,7 @@ INSERT INTO messages VALUES( false, now(), '22' -) ON CONFLICT (id) DO NOTHING; +); INSERT INTO messages VALUES( 6, @@ -148,7 +127,7 @@ INSERT INTO messages VALUES( false, now(), '31' -) ON CONFLICT (id) DO NOTHING; +); INSERT INTO messages VALUES( 7, @@ -156,7 +135,7 @@ INSERT INTO messages VALUES( false, now(), '32' -) ON CONFLICT (id) DO NOTHING; +); INSERT INTO messages VALUES( 8, @@ -164,4 +143,4 @@ INSERT INTO messages VALUES( true, now(), '32' -) ON CONFLICT (id) DO NOTHING; +); -- cgit v1.2.3 From c9aed176706852196fec0fc65ee7552d511316f2 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Thu, 9 Sep 2021 15:54:17 +0100 Subject: Move psql health check to docker compose file --- docker-compose.yml | 8 +++++++- manage.py | 52 ---------------------------------------------------- 2 files changed, 7 insertions(+), 53 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 05867a46..eb987624 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,6 +18,11 @@ services: POSTGRES_DB: pysite POSTGRES_PASSWORD: pysite POSTGRES_USER: pysite + healthcheck: + test: ["CMD-SHELL", "pg_isready -U pysite"] + interval: 2s + timeout: 1s + retries: 5 volumes: - ./postgres/init.sql:/docker-entrypoint-initdb.d/init.sql @@ -35,7 +40,8 @@ services: ports: - "127.0.0.1:8000:8000" depends_on: - - postgres + postgres: + condition: service_healthy tty: true volumes: - .:/app:ro diff --git a/manage.py b/manage.py index d8258281..578f4748 100755 --- a/manage.py +++ b/manage.py @@ -1,9 +1,6 @@ #!/usr/bin/env python import os -import socket import sys -import time -from urllib.parse import SplitResult, urlsplit import django from django.contrib.auth import get_user_model @@ -54,21 +51,6 @@ class SiteManager: os.environ.setdefault("DEBUG", "true") print("Starting in debug mode.") - @staticmethod - def parse_db_url(db_url: str) -> SplitResult: - """Validate and split the given databse url.""" - db_url_parts = urlsplit(db_url) - if not all(( - db_url_parts.hostname, - db_url_parts.username, - db_url_parts.password, - db_url_parts.path - )): - raise ValueError( - "The DATABASE_URL environment variable is not a valid PostgreSQL database URL." - ) - return db_url_parts - @staticmethod def create_superuser() -> None: """Create a default django admin super user in development environments.""" @@ -98,36 +80,6 @@ class SiteManager: else: print(f"Existing bot token found: {token}") - @staticmethod - def wait_for_postgres() -> None: - """Wait for the PostgreSQL database specified in DATABASE_URL.""" - print("Waiting for PostgreSQL database.") - - # Get database URL based on environmental variable passed in compose - database_url_parts = SiteManager.parse_db_url(os.environ["DATABASE_URL"]) - domain = database_url_parts.hostname - # Port may be omitted, 5432 is the default psql port - port = database_url_parts.port or 5432 - - # Attempt to connect to the database socket - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - attempts_left = 10 - while attempts_left: - try: - # Ignore 'incomplete startup packet' - s.connect((domain, port)) - s.shutdown(socket.SHUT_RDWR) - print("Database is ready.") - break - except socket.error: - attempts_left -= 1 - print("Not ready yet, retrying.") - time.sleep(0.5) - else: - print("Database could not be found, exiting.") - sys.exit(1) - @staticmethod def set_dev_site_name() -> None: """Set the development site domain in admin from default example.""" @@ -197,10 +149,6 @@ class SiteManager: def main() -> None: """Entry point for Django management script.""" - # Always run metricity init when in CI, indicated by the CI env var - if os.environ.get("CI", "false").lower() == "true": - SiteManager.wait_for_postgres() - # Use the custom site manager for launching the server if len(sys.argv) > 1 and sys.argv[1] == "run": SiteManager(sys.argv).run_server() -- cgit v1.2.3 From 2bf473ea1c22ea28de9cbc7a842c465a2f931b3c Mon Sep 17 00:00:00 2001 From: Janine vN Date: Sat, 18 Sep 2021 18:16:35 -0400 Subject: Remove code jam notice from front page --- pydis_site/templates/home/index.html | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/pydis_site/templates/home/index.html b/pydis_site/templates/home/index.html index 072e3817..8e52274d 100644 --- a/pydis_site/templates/home/index.html +++ b/pydis_site/templates/home/index.html @@ -9,21 +9,14 @@ {% block content %} {% include "base/navbar.html" %} - -
- - Summer Code Jam 2021 - -
-
-
+
{# Embedded Welcome video #} -
+
- - {# Code Jam banner #} -
- - Summer Code Jam 2021 - -
-- cgit v1.2.3 From a7afdf4f8742a3f3f61883e92604d0fe08c8f199 Mon Sep 17 00:00:00 2001 From: Janine vN Date: Sat, 18 Sep 2021 18:26:32 -0400 Subject: Update wording on main Events page --- pydis_site/templates/events/index.html | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pydis_site/templates/events/index.html b/pydis_site/templates/events/index.html index daad1c9c..352a3aa9 100644 --- a/pydis_site/templates/events/index.html +++ b/pydis_site/templates/events/index.html @@ -9,11 +9,8 @@ {% block event_content %}

Code Jams

-
- The 2021 Summer Code Jam qualifier will open June 21st. Check out the details here. -
-

Each year, we organize a Winter Code Jam and a Summer Code Jam. During these events, members of our community will work together in teams to create something amazing using a technology we picked for them. One such technology that was picked for the Winter Code Jam 2020 was Kivy, a cross-platform GUI framework.

-

To help fuel the creative process, we provide a specific theme, like Ancient Technology or This App Hates You. At the end of the Code Jam, the projects are judged by Python Discord server staff members and guest judges from the larger Python community. The judges will consider creativity, code quality, teamwork, and adherence to the theme.

+

Each year, we organize at least one code jam, one during the summer and sometimes one during the winter. During these events, members of our community will work together in teams to create something amazing using a technology we picked for them. One such technology that was picked for the Summer 2021 Code Jam was text user interfaces (TUIS), where teams could pick from a pre-approved list of frameworks.

+

To help fuel the creative process, we provide a specific theme, like Think Outside the Box or Early Internet. At the end of the Code Jam, the projects are judged by Python Discord server staff members and guest judges from the larger Python community. The judges will consider creativity, code quality, teamwork, and adherence to the theme.

If you want to read more about Code Jams, visit our Code Jam info page or watch this video showcasing the best projects created during the Winter Code Jam 2020: Ancient Technology:

@@ -121,6 +118,5 @@ {% endblock %} {% block sidebar %} - {% include "events/sidebar/ongoing-event.html" %} {% include "events/sidebar/events-list.html" %} {% endblock %} -- cgit v1.2.3 From d7d0f2de54720c257d066fce1bcfc5c66dde2d89 Mon Sep 17 00:00:00 2001 From: Janine vN Date: Sat, 18 Sep 2021 18:34:51 -0400 Subject: Update events featured Re-ordered and updated the events listed on the main events page. We're no longer participating in Hacktoberfest, so that was removed. Game jams was moved to the bottom as we have not hosted one and will not for the forseeable future. PyWeek was moved above Advent of Code because AoC is one of the last events we do in the year so it didn't make sense to have PyWeek below it. This also updated the Events Calendar on the sidebar to be accurate for 2021. --- pydis_site/templates/events/index.html | 64 +++++++--------------- .../templates/events/sidebar/events-list.html | 10 ++-- 2 files changed, 26 insertions(+), 48 deletions(-) diff --git a/pydis_site/templates/events/index.html b/pydis_site/templates/events/index.html index 352a3aa9..b5c556e5 100644 --- a/pydis_site/templates/events/index.html +++ b/pydis_site/templates/events/index.html @@ -16,48 +16,27 @@
-

Game Jam

+

PyWeek

- The Game Jam is similar to our Code Jams, but smaller in scope. Instead of having to complete a qualifier - and being teamed up with random strangers, members of our community can just sign-up individually or pair up - with whoever they like. -

-

- The participants will have ten days to create a game using the technology we've selected, and drawing - inspiration from a provided theme. After the event, a panel of judges will play all the games and select a - winner. The top 5 will featured in a special video on our YouTube channel. -

-

- The first edition of the Game Jam ran from - April 17, 2020 to April 26, 2020. + For the past 15 years, PyWeek has been running a bi-annual game jam for the + Python language. As of 2020, we are excited to say we are officially partnered with PyWeek to co-run these + events.

-
-
- -
-
-
- -
-

Hacktoberfest

-
-

- This event revolves around the annual Hacktoberfest - event organized by Digital Ocean. In addition to promoting Hacktoberfest in our community and supporting - those who choose to take their first steps into the world of open source, we will also ease our members into - contributing to open source by starting a low-entry, beginner-friendly open source project where we will - guide our members through the open source process in a safe environment. + During each PyWeek event, we open a special discussion channel in which our members can discuss their + submissions, meet other participants, and talk to PyWeek staff. The PyWeek organizer, + Daniel Pope (@lordmauve) will be present during the entire event to answer + questions and post announcements and information in our community.

- The exact form this event will take has not been decided yet, but we'll make sure to keep you updated in - our community announcements! + Unlike our other events, the community will select the winner from all the submissions + during PyWeek. We may release YouTube content showcasing the best submissions after the events are finished.

- +
@@ -91,27 +70,26 @@
-

PyWeek

+

Game Jam

- For the past 15 years, PyWeek has been running a bi-annual game jam for the - Python language. As of 2020, we are excited to say we are officially partnered with PyWeek to co-run these - events. + The Game Jam is similar to our Code Jams, but smaller in scope. Instead of having to complete a qualifier + and being teamed up with random strangers, members of our community can just sign-up individually or pair up + with whoever they like.

- During each PyWeek event, we open a special discussion channel in which our members can discuss their - submissions, meet other participants, and talk to PyWeek staff. The PyWeek organizer, - Daniel Pope (@lordmauve) will be present during the entire event to answer - questions and post announcements and information in our community. + The participants will have ten days to create a game using the technology we've selected, and drawing + inspiration from a provided theme. After the event, a panel of judges will play all the games and select a + winner. The top 5 will featured in a special video on our YouTube channel.

- Unlike our other events, the community will select the winner from all the submissions - during PyWeek. We may release YouTube content showcasing the best submissions after the events are finished. + The first edition of the Game Jam ran from + April 17, 2020 to April 26, 2020.

- +
diff --git a/pydis_site/templates/events/sidebar/events-list.html b/pydis_site/templates/events/sidebar/events-list.html index 327b0e77..6029ae74 100644 --- a/pydis_site/templates/events/sidebar/events-list.html +++ b/pydis_site/templates/events/sidebar/events-list.html @@ -1,10 +1,10 @@ -- cgit v1.2.3 From c4731bfe1ed27ee8047fa9226158e927ec8600f7 Mon Sep 17 00:00:00 2001 From: Janine vN Date: Sat, 18 Sep 2021 19:13:20 -0400 Subject: Update Code Jam 8 as a finished event This commit adds in the information about the winners of the code jam. Additionally, it adds in Code Jam 8 as a prior event in the sidebar. --- .../templates/events/pages/code-jams/8/_index.html | 52 +++++++++++++++++++--- .../sidebar/code-jams/previous-code-jams.html | 1 + 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/pydis_site/templates/events/pages/code-jams/8/_index.html b/pydis_site/templates/events/pages/code-jams/8/_index.html index 55bdc95b..c789adef 100644 --- a/pydis_site/templates/events/pages/code-jams/8/_index.html +++ b/pydis_site/templates/events/pages/code-jams/8/_index.html @@ -20,6 +20,50 @@ and walking through the program that your team has created.

+

Code Jam Winners

+

Congratulations to our winners and the two runner ups! Check out their projects below.

+

Perceptive Porcupines: WTPython!?

+

VV, Poppinawhile, ethansocal, Jeff Z, Cohan, ¯\_(ツ)_/¯

+

+ What the Python (wtpython) is a simple terminal user interface that allows you to explore relevant answers on Stackoverflow without leaving your terminal or IDE. When you get an error, all you have to do is swap python for wtpython. When your code hits an error, you'll see a textual interface for exploring relevant answers allowing you to stay focused and ship faster! +
+ Demo video +
+ Githib Repository +
+ +

+ +

Lovable Lobsters: Ultimate Tic Tac Toe

+

A5Rocks, Bast, Dacheat, mega_hirtz, CopOnTheRun, richphi

+

+ Thinking inside a box, that is inside a box, that is inside yet another box. + + The terminal program created by the Lovable Lobsters allows you to play Ultimate Tic Tac Toe right form your terminal. The really impressive part though? You can play with your friends and family over your network! Their program has a server-client set-up that lets you play with your friends and family from different computers. +
+ Demo video +
+ Githib Repository +
+

+ +

Robust Reindeer: Rubick's Cube

+

Björn, aaronshenhao, mathsman, Dude Saber, 詭異, Keppo

+

+ This submission is a Rubik's cube, rendered in a text user interface (that was a constraint) using the asciimatics package, and addressing the theme "thinking inside the box". + + Just like a real world Rubik's cube, you can move this cube around to look at it from all sides. And, of course, you can rotate the individual discs it is made up of to first scramble up the order and then to try and solve it into perfect ordering again. +
+ Githib Repository +
+

+ +

Submissions

+

+ 63 teams started out on July 9th 2021. By the end of the jam, 51 teams made project submissions. Check them all out here: +

+

+

Important Dates

  • Tuesday, June 15 - Form to submit theme suggestions opens
  • @@ -43,7 +87,7 @@ The qualifier is a coding challenge that you are required to complete before registering for the code jam. This is meant as a basic assessment of your skills to ensure you have enough python knowledge to effectively contribute in a team environment.

    -

    View the Qualifier

    View the Qualifier

    Please note the requirements for the qualifier.

      @@ -52,11 +96,7 @@
    • The Qualifier must be submitted through the Code Jam sign-up form.

    -

    Submissions

    -

    - 63 teams started out on July 9th 2021. By the end of the jam, 51 teams made project submissions. Check them all out here: -

    -

    +

    Prizes

    Our Code Jam Sponsors have provided prizes for the winners of the code jam. diff --git a/pydis_site/templates/events/sidebar/code-jams/previous-code-jams.html b/pydis_site/templates/events/sidebar/code-jams/previous-code-jams.html index 9f9ecd1a..393eec6f 100644 --- a/pydis_site/templates/events/sidebar/code-jams/previous-code-jams.html +++ b/pydis_site/templates/events/sidebar/code-jams/previous-code-jams.html @@ -1,6 +1,7 @@

    - +
diff --git a/pydis_site/templates/events/sidebar/code-jams/previous-code-jams.html b/pydis_site/templates/events/sidebar/code-jams/previous-code-jams.html index 393eec6f..21b2ccb4 100644 --- a/pydis_site/templates/events/sidebar/code-jams/previous-code-jams.html +++ b/pydis_site/templates/events/sidebar/code-jams/previous-code-jams.html @@ -1,7 +1,7 @@
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: http://img-prod-cms-rt-microsoft-com.akamaized.net/cms/api/am/imageFileData/RE2qVsJ?ver=3f74 +title_image: https://img-prod-cms-rt-microsoft-com.akamaized.net/cms/api/am/imageFileData/RE2qVsJ?ver=3f74 position: 4 urls: - icon: branding/youtube diff --git a/pydis_site/settings.py b/pydis_site/settings.py index d2cd8698..8d092ac9 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -201,7 +201,7 @@ else: PARENT_HOST = env('PARENT_HOST', default='pythondiscord.com') # Django REST framework -# http://www.django-rest-framework.org +# https://www.django-rest-framework.org REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.TokenAuthentication', diff --git a/pydis_site/templates/base/navbar.html b/pydis_site/templates/base/navbar.html index d7cb49b2..ed04995c 100644 --- a/pydis_site/templates/base/navbar.html +++ b/pydis_site/templates/base/navbar.html @@ -44,7 +44,7 @@ {# Patreon #} - +  Patreon diff --git a/pydis_site/templates/events/pages/code-jams/8/frameworks.html b/pydis_site/templates/events/pages/code-jams/8/frameworks.html index 34ac4f0a..1c02e38a 100644 --- a/pydis_site/templates/events/pages/code-jams/8/frameworks.html +++ b/pydis_site/templates/events/pages/code-jams/8/frameworks.html @@ -19,7 +19,7 @@
    -
  • Documentation Link
  • +
  • Documentation Link
  • Supports: Linux, Mac, other unix-like OS
  • Somewhat in-depth tutorial
  • Uses widgets in a fairly straight forward design
  • -- cgit v1.2.3 From b8466fcbb3325b590c4b6ebe508ee35d02dc4b6a Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Wed, 6 Oct 2021 02:17:14 +0400 Subject: Properly Resize Sponsor Logos Ensure sponsor logos don't go past the edge of the screen, and maintain aspect ratio when sized down. Signed-off-by: Hassan Abouelela --- pydis_site/static/css/home/index.css | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pydis_site/static/css/home/index.css b/pydis_site/static/css/home/index.css index ee6f6e4c..7ec8af74 100644 --- a/pydis_site/static/css/home/index.css +++ b/pydis_site/static/css/home/index.css @@ -215,12 +215,20 @@ h1 { } #sponsors .columns { + display: block; justify-content: center; margin: auto; max-width: 80%; } +#sponsors a { + margin: auto; + display: inline-block; +} + #sponsors img { - height: 5rem; - margin: auto 1rem; + width: auto; + height: auto; + + max-height: 5rem; } -- cgit v1.2.3 From bc1aae035a25dca1a894e22421f493175edebac2 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Wed, 6 Oct 2021 02:18:39 +0400 Subject: Adds Netlify To Sponsors Signed-off-by: Hassan Abouelela --- pydis_site/static/images/sponsors/netlify.png | Bin 0 -> 177462 bytes pydis_site/templates/home/index.html | 4 ++++ 2 files changed, 4 insertions(+) create mode 100644 pydis_site/static/images/sponsors/netlify.png diff --git a/pydis_site/static/images/sponsors/netlify.png b/pydis_site/static/images/sponsors/netlify.png new file mode 100644 index 00000000..0f14f385 Binary files /dev/null and b/pydis_site/static/images/sponsors/netlify.png differ diff --git a/pydis_site/templates/home/index.html b/pydis_site/templates/home/index.html index 8e52274d..77037ef8 100644 --- a/pydis_site/templates/home/index.html +++ b/pydis_site/templates/home/index.html @@ -187,6 +187,10 @@ StreamYard + + + Netlify + Cloudflare -- cgit v1.2.3 From 81054bdd73c937f184fef828a8f3bacd6d30caaa Mon Sep 17 00:00:00 2001 From: Kronifer <44979306+Kronifer@users.noreply.github.com> Date: Tue, 5 Oct 2021 18:14:02 -0500 Subject: Merge Gitpod Docs (#594) docs: Added site documentation for editing on Gitpod --- .../resources/guides/pydis-guides/contributing/sir-lancebot.md | 9 +++++++++ 1 file changed, 9 insertions(+) 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 068b08ae..60169c01 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 @@ -16,6 +16,15 @@ toc: 1 - [MacOS Installer](https://git-scm.com/download/mac) or `brew install git` - [Linux](https://git-scm.com/download/linux) +## 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: +```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. + ## 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. -- cgit v1.2.3 From 8f596f3b5fd44a502efccc021ab4c50908da3d67 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Wed, 6 Oct 2021 00:18:37 +0100 Subject: Add www.pythondiscord.com to ALLOWED_HOSTS (#601) --- pydis_site/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pydis_site/settings.py b/pydis_site/settings.py index d2cd8698..656676da 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -55,6 +55,7 @@ else: ALLOWED_HOSTS = env.list( 'ALLOWED_HOSTS', default=[ + 'www.pythondiscord.com', 'pythondiscord.com', 'admin.pythondiscord.com', 'api.pythondiscord.com', -- cgit v1.2.3 From b5b49ef0ea9209ecc140302879383b619e694541 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Wed, 6 Oct 2021 00:35:59 +0100 Subject: Update hosts.py to default to www --- pydis_site/hosts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pydis_site/hosts.py b/pydis_site/hosts.py index 5a837a8b..719e93cf 100644 --- a/pydis_site/hosts.py +++ b/pydis_site/hosts.py @@ -9,5 +9,6 @@ host_patterns = patterns( # Internal API ingress (cluster local) host(r'pydis-api', 'pydis_site.apps.api.urls', name='internal_api'), host(r'staff', 'pydis_site.apps.staff.urls', name='staff'), - host(r'.*', 'pydis_site.apps.home.urls', name=settings.DEFAULT_HOST) + host(r'www', 'pydis_site.apps.home.urls', name=settings.DEFAULT_HOST), + host(r'.*', 'pydis_site.apps.home.urls', name="fallback") ) -- cgit v1.2.3 From 6a129ca9272a4b85d8fe5b78efda91cee3a413f5 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Wed, 6 Oct 2021 22:47:12 +0100 Subject: Update ALLOWED_HOSTS to include internal address --- pydis_site/settings.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pydis_site/settings.py b/pydis_site/settings.py index 35afea22..d7b87f33 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -59,11 +59,7 @@ else: 'pythondiscord.com', gethostname(), gethostbyname(gethostname()), - # "That needs to be there for now, until we move back to... - # no, don't put that there, actually, yeah, put that there, - # that's fine, yeah, no no no no no no, stop it, you're being - # a problem now, I'm phoning [DAD'S NAME]" - Joe - 'pydis-api.default.svc.cluster.local', + 'site.default.svc.cluster.local', ], ) SECRET_KEY = env('SECRET_KEY') -- cgit v1.2.3 From bb23a46f84f724552ca554a93a2536e65f36db25 Mon Sep 17 00:00:00 2001 From: Vivaan Verma Date: Thu, 7 Oct 2021 21:27:11 +0100 Subject: Add a pull request template This pull request template was mainly added so that the second bullet point in [this PR comment](https://github.com/python-discord/api/pull/17#issuecomment-933739292) can be addressed. --- .github/PULL_REQUEST_TEMPLATE/pull_request.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE/pull_request.md diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request.md b/.github/PULL_REQUEST_TEMPLATE/pull_request.md new file mode 100644 index 00000000..34e56aec --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request.md @@ -0,0 +1,19 @@ +## Summary +This PR introduces [ brief summary ] in accordance with site#[ issue number ] + +## Description of changes +- `foo` was refactored to `bar` +- The baz feature was implemented +- ... + +### I confirm I have: +- [ ] Joined the [Python Discord community](discord.gg/python) +- [ ] Read the [Code of Conduct](https://www.pydis.com/pages/code-of-conduct) and agree to it +- [ ] I have discussed implementing this feature on the relevant service (Discord, GitHub, etc.) + +### If I have changed any models (edited files under the `site/apps/api/models` directory), I ensure I have: +- [ ] Opened a PR updating the model on the [API GitHub Repository](https://github.com/python-discord/api) + +**OR** + +- [ ] Opened an issue on the [API GitHub Repository](https://github.com/python-discord/api) explaining what changes need to be made -- cgit v1.2.3 From 3d97138f2b5f2bc52390e595fa805405ca21604e Mon Sep 17 00:00:00 2001 From: Vivaan Verma Date: Thu, 7 Oct 2021 21:37:32 +0100 Subject: chore: Add comments to instruct users --- .github/PULL_REQUEST_TEMPLATE/pull_request.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request.md b/.github/PULL_REQUEST_TEMPLATE/pull_request.md index 34e56aec..382ee2a7 100644 --- a/.github/PULL_REQUEST_TEMPLATE/pull_request.md +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request.md @@ -6,12 +6,16 @@ This PR introduces [ brief summary ] in accordance with site#[ issue number ] - The baz feature was implemented - ... + + ### I confirm I have: - [ ] Joined the [Python Discord community](discord.gg/python) - [ ] Read the [Code of Conduct](https://www.pydis.com/pages/code-of-conduct) and agree to it - [ ] I have discussed implementing this feature on the relevant service (Discord, GitHub, etc.) -### If I have changed any models (edited files under the `site/apps/api/models` directory), I ensure I have: + + +### I have changed API models and I ensure I have: - [ ] Opened a PR updating the model on the [API GitHub Repository](https://github.com/python-discord/api) **OR** -- cgit v1.2.3 From 1bb70f38cb6da64df3b46688e81b860b172ad49d Mon Sep 17 00:00:00 2001 From: Vivaan Verma Date: Thu, 7 Oct 2021 21:50:00 +0100 Subject: chore: Replace examples with comments --- .github/PULL_REQUEST_TEMPLATE/pull_request.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request.md b/.github/PULL_REQUEST_TEMPLATE/pull_request.md index 382ee2a7..34359cec 100644 --- a/.github/PULL_REQUEST_TEMPLATE/pull_request.md +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request.md @@ -1,21 +1,20 @@ ## Summary -This PR introduces [ brief summary ] in accordance with site#[ issue number ] + + ## Description of changes -- `foo` was refactored to `bar` -- The baz feature was implemented -- ... + - ### I confirm I have: + - [ ] Joined the [Python Discord community](discord.gg/python) - [ ] Read the [Code of Conduct](https://www.pydis.com/pages/code-of-conduct) and agree to it - [ ] I have discussed implementing this feature on the relevant service (Discord, GitHub, etc.) - ### I have changed API models and I ensure I have: + - [ ] Opened a PR updating the model on the [API GitHub Repository](https://github.com/python-discord/api) **OR** -- cgit v1.2.3 From b57c7e8e8f96ffc0ab031e3f7f51592c0cee0d98 Mon Sep 17 00:00:00 2001 From: Vivaan Verma Date: Thu, 7 Oct 2021 22:09:49 +0100 Subject: fix: clarify descirption of changes section --- .github/PULL_REQUEST_TEMPLATE/pull_request.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request.md b/.github/PULL_REQUEST_TEMPLATE/pull_request.md index 34359cec..358d2553 100644 --- a/.github/PULL_REQUEST_TEMPLATE/pull_request.md +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request.md @@ -3,7 +3,7 @@ ## Description of changes - + ### I confirm I have: -- cgit v1.2.3 From afec45b596568817d8921e9c0eb355db95bf10a7 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Wed, 6 Oct 2021 23:01:48 +0200 Subject: Decrease page size for user list endpoint. From some debugging via python-discord/kubernetes#96, we have determined that this endpoint results in high latencies to the site and eventually Kubernetes killing off site pods randomly. --- pydis_site/apps/api/viewsets/bot/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 0356e193..22d13dc4 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -19,7 +19,7 @@ from pydis_site.apps.api.serializers import UserSerializer class UserListPagination(PageNumberPagination): """Custom pagination class for the User Model.""" - page_size = 10000 + page_size = 2500 page_size_query_param = "page_size" def get_next_page_number(self) -> typing.Optional[int]: -- cgit v1.2.3 From 4c527e702a90bd108844ed59f93f41cbc3631df6 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sat, 9 Oct 2021 18:17:53 +0200 Subject: Automatically adjust test to `page_size`. --- pydis_site/apps/api/tests/test_users.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py index 77876d6f..295bcf64 100644 --- a/pydis_site/apps/api/tests/test_users.py +++ b/pydis_site/apps/api/tests/test_users.py @@ -6,6 +6,7 @@ from django.urls import reverse from .base import AuthenticatedAPITestCase from ..models import Role, User from ..models.bot.metricity import NotFoundError +from ..viewsets.bot.user import UserListPagination class UnauthedUserAPITests(AuthenticatedAPITestCase): @@ -357,7 +358,7 @@ class UserPaginatorTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): users = [] - for i in range(1, 10_001): + for i in range(1, UserListPagination.page_size + 1): users.append(User( id=i, name=f"user{i}", @@ -373,9 +374,10 @@ class UserPaginatorTests(AuthenticatedAPITestCase): self.assertIsNone(response["previous_page_no"]) def test_returns_next_page_number(self): + user_id = UserListPagination.page_size + 1 User.objects.create( - id=10_001, - name="user10001", + id=user_id, + name=f"user{user_id}", discriminator=1111, in_guild=True ) @@ -384,9 +386,10 @@ class UserPaginatorTests(AuthenticatedAPITestCase): self.assertEqual(2, response["next_page_no"]) def test_returns_previous_page_number(self): + user_id = UserListPagination.page_size + 1 User.objects.create( - id=10_001, - name="user10001", + id=user_id, + name=f"user{user_id}", discriminator=1111, in_guild=True ) -- cgit v1.2.3 From e9c4cdf56e2efd65786a4cf4aee0bb4e4e56bc95 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Sun, 10 Oct 2021 01:28:39 +0300 Subject: Adds Django Distill To Project Adds django-distill to dependencies, and lays the basic groundwork to start building static routes. Adds a poetry task to help with testing. --- manage.py | 31 +++++++++++++++++++++++++++++++ poetry.lock | 17 ++++++++++++++++- pydis_site/settings.py | 21 ++++++++++++++++----- pyproject.toml | 2 ++ 4 files changed, 65 insertions(+), 6 deletions(-) diff --git a/manage.py b/manage.py index 578f4748..357134ec 100755 --- a/manage.py +++ b/manage.py @@ -1,6 +1,8 @@ #!/usr/bin/env python import os +import platform import sys +from pathlib import Path import django from django.contrib.auth import get_user_model @@ -147,6 +149,22 @@ class SiteManager: gunicorn.app.wsgiapp.run() +def clean_up_static_files(build_folder: Path) -> None: + """Recursively loop over the build directory and fix links.""" + for file in build_folder.iterdir(): + if file.is_dir(): + clean_up_static_files(file) + elif file.name.endswith(".html"): + # Fix parent host url + new = file.read_text(encoding="utf-8").replace(f"//{os.getenv('PARENT_HOST')}", "") + + # Fix windows paths if on windows + if platform.system() == "Windows": + new = new.replace("%5C", "/") + + file.write_text(new, encoding="utf-8") + + def main() -> None: """Entry point for Django management script.""" # Use the custom site manager for launching the server @@ -155,8 +173,21 @@ def main() -> None: # Pass any others directly to standard management commands else: + if _static_build := "distill" in sys.argv[1]: + # Build a static version of the site with no databases and API support + os.environ["STATIC_BUILD"] = "True" + if not os.getenv("PARENT_HOST"): + os.environ["PARENT_HOST"] = "REPLACE_THIS.HOST" + execute_from_command_line(sys.argv) + if _static_build: + # Clean up parent host in generated files + for arg in sys.argv[2:]: + if not arg.startswith("-"): + clean_up_static_files(Path(arg)) + break + if __name__ == '__main__': main() diff --git a/poetry.lock b/poetry.lock index c6724cfc..eac58fdb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -137,6 +137,18 @@ sqlparse = ">=0.2.2" argon2 = ["argon2-cffi (>=16.1.0)"] bcrypt = ["bcrypt"] +[[package]] +name = "django-distill" +version = "2.9.0" +description = "Static site renderer and publisher for Django." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +django = "*" +requests = "*" + [[package]] name = "django-environ" version = "0.4.5" @@ -757,7 +769,7 @@ brotli = ["brotli"] [metadata] lock-version = "1.1" python-versions = "3.9.*" -content-hash = "ed7da8dbc905d4f2c47e01301b49c4aed0083bee269da0ee5ebcc3abee4ab1a0" +content-hash = "9f0c069c14e2dbff63d58474702693f0c02b8cfd30e5af38303975a73b71bcfd" [metadata.files] asgiref = [ @@ -858,6 +870,9 @@ django = [ {file = "Django-3.0.14-py3-none-any.whl", hash = "sha256:9bc7aa619ed878fedba62ce139abe663a147dccfd20e907725ec11e02a1ca225"}, {file = "Django-3.0.14.tar.gz", hash = "sha256:d58d8394036db75a81896037d757357e79406e8f68816c3e8a28721c1d9d4c11"}, ] +django-distill = [ + {file = "django-distill-2.9.0.tar.gz", hash = "sha256:08f31dcde2e79e73c0bc4f36941830603a811cc89472be11f79f14affb460d84"}, +] 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"}, diff --git a/pydis_site/settings.py b/pydis_site/settings.py index d7b87f33..d38c298b 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -25,7 +25,8 @@ from pydis_site.constants import GIT_SHA env = environ.Env( DEBUG=(bool, False), SITE_DSN=(str, ""), - BUILDING_DOCKER=(bool, False) + BUILDING_DOCKER=(bool, False), + STATIC_BUILD=(bool, False), ) sentry_sdk.init( @@ -65,10 +66,14 @@ else: SECRET_KEY = env('SECRET_KEY') # Application definition -INSTALLED_APPS = [ +NON_STATIC_APPS = [ 'pydis_site.apps.api', - 'pydis_site.apps.home', 'pydis_site.apps.staff', +] if not env("STATIC_BUILD") else [] + +INSTALLED_APPS = [ + *NON_STATIC_APPS, + 'pydis_site.apps.home', 'pydis_site.apps.resources', 'pydis_site.apps.content', 'pydis_site.apps.events', @@ -86,14 +91,20 @@ INSTALLED_APPS = [ 'django_simple_bulma', 'rest_framework', 'rest_framework.authtoken', + + 'django_distill', ] if not env("BUILDING_DOCKER"): INSTALLED_APPS.append("django_prometheus") +NON_STATIC_MIDDLEWARE = [ + 'django_prometheus.middleware.PrometheusBeforeMiddleware', +] if not env("STATIC_BUILD") else [] + # Ensure that Prometheus middlewares are first and last here. MIDDLEWARE = [ - 'django_prometheus.middleware.PrometheusBeforeMiddleware', + *NON_STATIC_MIDDLEWARE, 'django.middleware.security.SecurityMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware', @@ -134,7 +145,7 @@ WSGI_APPLICATION = 'pydis_site.wsgi.application' DATABASES = { 'default': env.db(), 'metricity': env.db('METRICITY_DB_URL'), -} +} if not env("STATIC_BUILD") else {} # Password validation # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators diff --git a/pyproject.toml b/pyproject.toml index d0beb632..2f1322e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ sentry-sdk = "~=0.19" markdown = "~=3.3.4" python-frontmatter = "~=1.0" django-prometheus = "~=2.1" +django-distill = "~=2.9.0" [tool.poetry.dev-dependencies] coverage = "~=5.0" @@ -53,3 +54,4 @@ test = "coverage run manage.py test" report = "coverage report -m" lint = "pre-commit run --all-files" precommit = "pre-commit install" +static = "python mange.py distill-local build --traceback --force" -- cgit v1.2.3 From 247c4ba220f9ee73767f4aac2737e520e7ec4b07 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Sun, 10 Oct 2021 01:30:06 +0300 Subject: Adds Static Route Configurations Adds configuration which specifies how routes should be handled when building a static preview. --- pydis_site/apps/content/urls.py | 43 +++++++++++++++++++++++++++++++++++--- pydis_site/apps/events/urls.py | 35 ++++++++++++++++++++++++++++--- pydis_site/apps/home/urls.py | 6 +++--- pydis_site/apps/home/views/home.py | 26 ++++++++++++++++------- pydis_site/apps/resources/urls.py | 22 ++++++++++++++++--- pydis_site/urls.py | 11 ++++++++-- 6 files changed, 122 insertions(+), 21 deletions(-) diff --git a/pydis_site/apps/content/urls.py b/pydis_site/apps/content/urls.py index c11b222a..fe7c2852 100644 --- a/pydis_site/apps/content/urls.py +++ b/pydis_site/apps/content/urls.py @@ -1,9 +1,46 @@ -from django.urls import path +import typing +from pathlib import Path + +from django_distill import distill_path from . import views app_name = "content" + + +def __get_all_files(root: Path, folder: typing.Optional[Path] = None) -> list[str]: + """Find all folders and markdown files recursively starting from `root`.""" + if not folder: + folder = root + + results = [] + + for item in folder.iterdir(): + name = item.relative_to(root).__str__().replace("\\", "/") + + if item.is_dir(): + results.append(name) + results.extend(__get_all_files(root, item)) + else: + path, extension = name.rsplit(".", maxsplit=1) + if extension == "md": + results.append(path) + + return results + + +def get_all_pages() -> typing.Iterator[dict[str, str]]: + """Yield a dict of all pag categories.""" + for location in __get_all_files(Path("pydis_site", "apps", "content", "resources")): + yield {"location": location} + + urlpatterns = [ - path("", views.PageOrCategoryView.as_view(), name='pages'), - path("/", views.PageOrCategoryView.as_view(), name='page_category'), + distill_path("", views.PageOrCategoryView.as_view(), name='pages'), + distill_path( + "/", + views.PageOrCategoryView.as_view(), + name='page_category', + distill_func=get_all_pages + ), ] diff --git a/pydis_site/apps/events/urls.py b/pydis_site/apps/events/urls.py index 9a65cf1f..7ea65a31 100644 --- a/pydis_site/apps/events/urls.py +++ b/pydis_site/apps/events/urls.py @@ -1,9 +1,38 @@ -from django.urls import path +import typing +from pathlib import Path + +from django_distill import distill_path from pydis_site.apps.events.views import IndexView, PageView app_name = "events" + + +def __get_all_files(root: Path, folder: typing.Optional[Path] = None) -> list[str]: + """Find all folders and HTML files recursively starting from `root`.""" + if not folder: + folder = root + + results = [] + + for sub_folder in folder.iterdir(): + results.append( + sub_folder.relative_to(root).__str__().replace("\\", "/").replace(".html", "") + ) + + if sub_folder.is_dir(): + results.extend(__get_all_files(root, sub_folder)) + + return results + + +def get_all_events() -> typing.Iterator[dict[str, str]]: + """Yield a dict of all event pages.""" + for file in __get_all_files(Path("pydis_site", "templates", "events", "pages")): + yield {"path": file} + + urlpatterns = [ - path("", IndexView.as_view(), name="index"), - path("/", PageView.as_view(), name="page"), + distill_path("", IndexView.as_view(), name="index"), + distill_path("/", PageView.as_view(), name="page", distill_func=get_all_events), ] diff --git a/pydis_site/apps/home/urls.py b/pydis_site/apps/home/urls.py index 57abc942..30321ece 100644 --- a/pydis_site/apps/home/urls.py +++ b/pydis_site/apps/home/urls.py @@ -1,9 +1,9 @@ -from django.urls import path +from django_distill import distill_path from .views import HomeView, timeline app_name = 'home' urlpatterns = [ - path('', HomeView.as_view(), name='home'), - path('timeline/', timeline, name="timeline"), + distill_path('', HomeView.as_view(), name='home'), + distill_path('timeline/', timeline, name="timeline"), ] diff --git a/pydis_site/apps/home/views/home.py b/pydis_site/apps/home/views/home.py index 401c768f..e28a3a00 100644 --- a/pydis_site/apps/home/views/home.py +++ b/pydis_site/apps/home/views/home.py @@ -8,6 +8,7 @@ from django.shortcuts import render from django.utils import timezone from django.views import View +from pydis_site import settings from pydis_site.apps.home.models import RepositoryMetadata from pydis_site.constants import GITHUB_TOKEN, TIMEOUT_PERIOD @@ -32,7 +33,10 @@ class HomeView(View): def __init__(self): """Clean up stale RepositoryMetadata.""" - RepositoryMetadata.objects.exclude(repo_name__in=self.repos).delete() + self._static_build = settings.env("STATIC_BUILD") + + if not self._static_build: + RepositoryMetadata.objects.exclude(repo_name__in=self.repos).delete() # If no token is defined (for example in local development), then # it does not make sense to pass the Authorization header. More @@ -91,10 +95,13 @@ class HomeView(View): def _get_repo_data(self) -> List[RepositoryMetadata]: """Build a list of RepositoryMetadata objects that we can use to populate the front page.""" # First off, load the timestamp of the least recently updated entry. - last_update = ( - RepositoryMetadata.objects.values_list("last_updated", flat=True) - .order_by("last_updated").first() - ) + if self._static_build: + last_update = None + else: + last_update = ( + RepositoryMetadata.objects.values_list("last_updated", flat=True) + .order_by("last_updated").first() + ) # If we did not retrieve any results here, we should import them! if last_update is None: @@ -104,7 +111,7 @@ class HomeView(View): api_repositories = self._get_api_data() # Create all the repodata records in the database. - return RepositoryMetadata.objects.bulk_create( + data = [ RepositoryMetadata( repo_name=api_data["full_name"], description=api_data["description"], @@ -113,7 +120,12 @@ class HomeView(View): language=api_data["language"], ) for api_data in api_repositories.values() - ) + ] + + if settings.env("STATIC_BUILD"): + return data + else: + return RepositoryMetadata.objects.bulk_create(data) # If the data is stale, we should refresh it. if (timezone.now() - last_update).seconds > self.repository_cache_ttl: diff --git a/pydis_site/apps/resources/urls.py b/pydis_site/apps/resources/urls.py index 19142081..10eda132 100644 --- a/pydis_site/apps/resources/urls.py +++ b/pydis_site/apps/resources/urls.py @@ -1,9 +1,25 @@ -from django.urls import path +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 = [ - path("", views.ResourcesView.as_view(), name="index"), - path("/", views.ResourcesListView.as_view(), name="resources") + distill_path("", views.ResourcesView.as_view(), name="index"), + distill_path( + "/", + views.ResourcesListView.as_view(), + name="resources", + distill_func=get_all_resources + ), ] diff --git a/pydis_site/urls.py b/pydis_site/urls.py index 891dbdcc..51ef4214 100644 --- a/pydis_site/urls.py +++ b/pydis_site/urls.py @@ -1,8 +1,9 @@ from django.contrib import admin from django.urls import include, path +from pydis_site import settings -urlpatterns = ( +NON_STATIC_PATTERNS = [ path('admin/', admin.site.urls), # External API ingress (over the net) @@ -14,9 +15,15 @@ urlpatterns = ( # 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, 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('staff/', include('pydis_site.apps.staff.urls', namespace='staff')), path('', include('pydis_site.apps.home.urls', namespace='home')), ) -- cgit v1.2.3 From 595185d826286b7b832d66a5b35e0f7ecbe29c55 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Sun, 10 Oct 2021 01:30:55 +0300 Subject: Adds Static Docker Build Adds a docker image which performs a static build of the site. --- static-builds/Dockerfile | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 static-builds/Dockerfile diff --git a/static-builds/Dockerfile b/static-builds/Dockerfile new file mode 100644 index 00000000..a70165a0 --- /dev/null +++ b/static-builds/Dockerfile @@ -0,0 +1,28 @@ +# Build a static preview of the site +FROM --platform=linux/amd64 python:3.9-slim-buster + +# Allow service to handle stops gracefully +STOPSIGNAL SIGQUIT + +# Set pip to have cleaner logs and no saved cache +ENV PIP_NO_CACHE_DIR=false \ + POETRY_VIRTUALENVS_CREATE=false + +# Install poetry +RUN pip install -U poetry + +# Copy the project files into working directory +WORKDIR /app + +# Install project dependencies +COPY pyproject.toml poetry.lock ./ +RUN poetry install --no-dev + +ARG git_sha="development" +ENV GIT_SHA=$git_sha +ENV PARENT_HOST=replace_me.host + +# Copy the source code in last to optimize rebuilding the image +COPY . . + +RUN SECRET_KEY=dummy_value python manage.py distill-local build --traceback --force --collectstatic -- cgit v1.2.3 From f9ba4a8bca526cb66b513622b987f775aa88403e Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Sun, 10 Oct 2021 01:31:36 +0300 Subject: Adds Netlify Builds Adds an action which builds and uploads the static site as an artifact, and a fetch script to be run on the netlify builders. --- .github/workflows/static-preview.yaml | 69 +++++++++++++++++++ .gitignore | 3 + static-builds/README.md | 47 +++++++++++++ static-builds/netlify_build.py | 123 ++++++++++++++++++++++++++++++++++ 4 files changed, 242 insertions(+) create mode 100644 .github/workflows/static-preview.yaml create mode 100644 static-builds/README.md create mode 100644 static-builds/netlify_build.py diff --git a/.github/workflows/static-preview.yaml b/.github/workflows/static-preview.yaml new file mode 100644 index 00000000..50deed6d --- /dev/null +++ b/.github/workflows/static-preview.yaml @@ -0,0 +1,69 @@ +name: Build & Publish Static Preview + +on: + push: + branches: + - main + pull_request: + +jobs: + build: + name: Build Static Preview + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + # Create a commit SHA-based tag for the container repositories + - name: Create SHA Container Tag + id: sha_tag + run: | + tag=$(cut -c 1-7 <<< $GITHUB_SHA) + echo "::set-output name=tag::$tag" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Login to Github Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Build the container, including an inline cache manifest to + # allow us to use the registry as a cache source. + - name: Build & Push Base Image + uses: docker/build-push-action@v2 + if: github.ref == 'refs/heads/main' + with: + context: . + file: ./static-builds/Dockerfile + push: true + cache-from: type=registry,ref=ghcr.io/python-discord/static-site:latest + cache-to: type=inline + tags: | + ghcr.io/python-discord/static-site:latest + ghcr.io/python-discord/static-site:${{ steps.sha_tag.outputs.tag }} + build-args: | + git_sha=${{ github.sha }} + + - name: Build Local Docker Image + run: | + docker build \ + --build-arg git_sha=${{ github.sha }} \ + --cache-from ghcr.io/python-discord/static-site:latest \ + -t static-site:${{ steps.sha_tag.outputs.tag }} \ + -f static-builds/Dockerfile \ + . + + - name: Extract Build From Docker Image + run: | + docker run --name site static-site:${{ steps.sha_tag.outputs.tag }} \ + && docker cp site:/app/build build/ + + - name: Upload Build + uses: actions/upload-artifact@v2 + with: + name: static-build + path: build/ diff --git a/.gitignore b/.gitignore index e4ad2c19..08d257bc 100644 --- a/.gitignore +++ b/.gitignore @@ -126,3 +126,6 @@ staticfiles/ *.js.tmp log.* + +# Local Netlify folder +.netlify diff --git a/static-builds/README.md b/static-builds/README.md new file mode 100644 index 00000000..ee2c0910 --- /dev/null +++ b/static-builds/README.md @@ -0,0 +1,47 @@ +# Static Builds +This directory includes all the needed information to build and deploy static previews of the site. + +Static deployments use [django-distill](https://github.com/meeb/django-distill) to build the static content. +The content is built in GitHub Actions, and is fetched and deployed by Netlify. + + +## Instructions +These are the configuration instructions to get started with static deployments. +They are split into two parts: + +- [Building The Site](#building-the-site) +- [Deploying To Netlify](#deploying-to-netlify) + + +### Building The Site +To get started with building, you can use the following command: + +```shell +python -m pip install httpx==0.19.0 +python manage.py distill-local build --traceback --force --collectstatic +``` + +Alternatively, you can use the [Dockerfile](./Dockerfile) in this folder. + +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. + + +### Deploying To Netlify +To deploy to netlify, link your site GitHub repository to a netlify site, and use the following settings: + +Build Command: +`python -m pip install httpx==0.19.0 && python static-builds/netlify_build.py` + +Publish Directory: +`build` + +Environment Variables: +- PYTHON_VERSION: 3.8 +- TOKEN: A GitHub token with access to download build artifacts. + + +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. diff --git a/static-builds/netlify_build.py b/static-builds/netlify_build.py new file mode 100644 index 00000000..6686e2ab --- /dev/null +++ b/static-builds/netlify_build.py @@ -0,0 +1,123 @@ +"""Build script to deploy project on netlify.""" + +# WARNING: This file must remain compatible with python 3.8 + +# This script performs all the actions required to build and deploy our project on netlify +# It requires the following environment variable: + +# TOKEN: A GitHub access token that can download the artifact. +# For PAT, the only scope needed is `public_repos` + +# It depends on the following packages, which are set in the netlify UI: +# httpx == 0.19.0 + +import os +import time +import zipfile +from pathlib import Path +from urllib import parse + +import httpx + +API_URL = "https://api.github.com" +OWNER, REPO = parse.urlparse(os.getenv("REPOSITORY_URL")).path.lstrip("/").split("/")[0:2] + + +def get_build_artifact() -> str: + """Search for a build artifact, and return the download URL.""" + print("Fetching build URL.") + + if os.getenv("PULL_REQUEST").lower() == "true": + print(f"Fetching data for PR #{os.getenv('REVIEW_ID')}") + + pull_url = f"{API_URL}/repos/{OWNER}/{REPO}/pulls/{os.getenv('REVIEW_ID')}" + pull_request = httpx.get(pull_url) + pull_request.raise_for_status() + + commit_sha = pull_request.json()["head"]["sha"] + + workflows_params = parse.urlencode({ + "event": "pull_request", + "per_page": 100 + }) + + else: + commit_sha = os.getenv("COMMIT_REF") + + workflows_params = parse.urlencode({ + "event": "push", + "per_page": 100 + }) + + print(f"Fetching action data for commit {commit_sha}") + + workflows = httpx.get(f"{API_URL}/repos/{OWNER}/{REPO}/actions/runs?{workflows_params}") + workflows.raise_for_status() + + for run in workflows.json()["workflow_runs"]: + if run["name"] == "Build & Publish Static Preview" and commit_sha == run["head_sha"]: + print(f"Found action for this commit: {run['id']}\n{run['html_url']}") + break + else: + raise Exception("Could not find the workflow run for this event.") + + polls = 0 + while polls <= 20: + if run["status"] != "completed": + print("Action isn't completed, sleeping for 30 seconds.") + polls += 1 + time.sleep(30) + + elif run["conclusion"] != "success": + print("Aborting build due to a failure in a previous CI step.") + exit(0) + + else: + print(f"Found artifact URL:\n{run['artifacts_url']}") + return run["artifacts_url"] + + _run = httpx.get(run["url"]) + _run.raise_for_status() + run = _run.json() + + raise Exception("Polled for the artifact workflow, but it was not ready in time.") + + +def download_artifact(url: str) -> None: + """Download a build artifact from `url`, and unzip the content.""" + print("Fetching artifact data.") + + artifacts = httpx.get(url) + artifacts.raise_for_status() + artifacts = artifacts.json() + + if artifacts["total_count"] == "0": + raise Exception(f"No artifacts were found for this build, aborting.\n{url}") + + for artifact in artifacts["artifacts"]: + if artifact["name"] == "static-build": + print("Found artifact with build.") + break + else: + raise Exception("Could not find an artifact with the expected name.") + + zipped_content = httpx.get(artifact["archive_download_url"], headers={ + "Authorization": f"token {os.getenv('TOKEN')}" + }) + zipped_content.raise_for_status() + + zip_file = Path("temp.zip") + zip_file.write_bytes(zipped_content.read()) + + with zipfile.ZipFile(zip_file, "r") as zip_ref: + zip_ref.extractall("build") + + zip_file.unlink(missing_ok=True) + + print("Wrote artifact content to target directory.") + + +if __name__ == "__main__": + print("Build started") + artifact_url = get_build_artifact() + download_artifact(artifact_url) -- cgit v1.2.3 From cf199b2b84773d568d7f548fdbf6ba12f63171c3 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Sun, 10 Oct 2021 13:23:02 +0300 Subject: Fixes Caching On Docker Build Reworks the docker build action to use buildx in all steps to make the caching work. Reduces the wait time on the fetch action. Signed-off-by: Hassan Abouelela --- .github/workflows/static-preview.yaml | 32 +++++++++++++++++++------------- static-builds/netlify_build.py | 4 ++-- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/.github/workflows/static-preview.yaml b/.github/workflows/static-preview.yaml index 50deed6d..970fad99 100644 --- a/.github/workflows/static-preview.yaml +++ b/.github/workflows/static-preview.yaml @@ -33,7 +33,7 @@ jobs: # Build the container, including an inline cache manifest to # allow us to use the registry as a cache source. - - name: Build & Push Base Image + - name: Build Docker Image (Main) uses: docker/build-push-action@v2 if: github.ref == 'refs/heads/main' with: @@ -48,22 +48,28 @@ jobs: build-args: | git_sha=${{ github.sha }} - - name: Build Local Docker Image + - name: Extract Build From Docker Image (Main) + if: github.ref == 'refs/heads/main' run: | - docker build \ - --build-arg git_sha=${{ github.sha }} \ - --cache-from ghcr.io/python-discord/static-site:latest \ - -t static-site:${{ steps.sha_tag.outputs.tag }} \ - -f static-builds/Dockerfile \ - . + mkdir docker_build \ + && docker run --name site ghcr.io/python-discord/static-site:${{ steps.sha_tag.outputs.tag }} \ + && docker cp site:/app docker_build/ - - name: Extract Build From Docker Image - run: | - docker run --name site static-site:${{ steps.sha_tag.outputs.tag }} \ - && docker cp site:/app/build build/ + # Build directly to a local folder + - name: Build Docker Image (PR) + uses: docker/build-push-action@v2 + if: github.ref != 'refs/heads/main' + with: + context: . + file: ./static-builds/Dockerfile + push: false + cache-from: type=registry,ref=ghcr.io/python-discord/static-site:latest + outputs: type=local,dest=docker_build/ + build-args: | + git_sha=${{ github.sha }} - name: Upload Build uses: actions/upload-artifact@v2 with: name: static-build - path: build/ + path: docker_build/app/build/ diff --git a/static-builds/netlify_build.py b/static-builds/netlify_build.py index 6686e2ab..5699c3e4 100644 --- a/static-builds/netlify_build.py +++ b/static-builds/netlify_build.py @@ -64,9 +64,9 @@ def get_build_artifact() -> str: polls = 0 while polls <= 20: if run["status"] != "completed": - print("Action isn't completed, sleeping for 30 seconds.") + print("Action isn't ready, sleeping for 10 seconds.") polls += 1 - time.sleep(30) + time.sleep(10) elif run["conclusion"] != "success": print("Aborting build due to a failure in a previous CI step.") -- cgit v1.2.3 From 79259eb856ea2847a61c44dee4eb03f557a7e4f1 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Sun, 10 Oct 2021 14:42:40 +0300 Subject: Merges Dockerfiles Merges the normal dockerfile with the static build one to reduce duplication. Signed-off-by: Hassan Abouelela --- .github/workflows/static-preview.yaml | 7 ++++--- Dockerfile | 6 ++++++ static-builds/Dockerfile | 28 ---------------------------- static-builds/README.md | 2 +- 4 files changed, 11 insertions(+), 32 deletions(-) delete mode 100644 static-builds/Dockerfile diff --git a/.github/workflows/static-preview.yaml b/.github/workflows/static-preview.yaml index 970fad99..06192f02 100644 --- a/.github/workflows/static-preview.yaml +++ b/.github/workflows/static-preview.yaml @@ -38,7 +38,6 @@ jobs: if: github.ref == 'refs/heads/main' with: context: . - file: ./static-builds/Dockerfile push: true cache-from: type=registry,ref=ghcr.io/python-discord/static-site:latest cache-to: type=inline @@ -47,12 +46,14 @@ jobs: ghcr.io/python-discord/static-site:${{ steps.sha_tag.outputs.tag }} build-args: | git_sha=${{ github.sha }} + STATIC_BUILD=TRUE - name: Extract Build From Docker Image (Main) if: github.ref == 'refs/heads/main' run: | mkdir docker_build \ - && docker run --name site ghcr.io/python-discord/static-site:${{ steps.sha_tag.outputs.tag }} \ + && docker run --entrypoint /bin/echo --name site \ + ghcr.io/python-discord/static-site:${{ steps.sha_tag.outputs.tag }} \ && docker cp site:/app docker_build/ # Build directly to a local folder @@ -61,12 +62,12 @@ jobs: if: github.ref != 'refs/heads/main' with: context: . - file: ./static-builds/Dockerfile push: false cache-from: type=registry,ref=ghcr.io/python-discord/static-site:latest outputs: type=local,dest=docker_build/ build-args: | git_sha=${{ github.sha }} + STATIC_BUILD=TRUE - name: Upload Build uses: actions/upload-artifact@v2 diff --git a/Dockerfile b/Dockerfile index 046e7f80..2b039fab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,6 +36,12 @@ RUN \ METRICITY_DB_URL=postgres://localhost \ python manage.py collectstatic --noinput --clear +# Build static files if we are doing a static build +ARG STATIC_BUILD=false +RUN if [ $STATIC_BUILD = "TRUE" ] ; \ + then SECRET_KEY=dummy_value python manage.py distill-local build --traceback --force ; \ +fi + # Run web server through custom manager ENTRYPOINT ["python", "manage.py"] CMD ["run"] diff --git a/static-builds/Dockerfile b/static-builds/Dockerfile deleted file mode 100644 index a70165a0..00000000 --- a/static-builds/Dockerfile +++ /dev/null @@ -1,28 +0,0 @@ -# Build a static preview of the site -FROM --platform=linux/amd64 python:3.9-slim-buster - -# Allow service to handle stops gracefully -STOPSIGNAL SIGQUIT - -# Set pip to have cleaner logs and no saved cache -ENV PIP_NO_CACHE_DIR=false \ - POETRY_VIRTUALENVS_CREATE=false - -# Install poetry -RUN pip install -U poetry - -# Copy the project files into working directory -WORKDIR /app - -# Install project dependencies -COPY pyproject.toml poetry.lock ./ -RUN poetry install --no-dev - -ARG git_sha="development" -ENV GIT_SHA=$git_sha -ENV PARENT_HOST=replace_me.host - -# Copy the source code in last to optimize rebuilding the image -COPY . . - -RUN SECRET_KEY=dummy_value python manage.py distill-local build --traceback --force --collectstatic diff --git a/static-builds/README.md b/static-builds/README.md index ee2c0910..fe24df07 100644 --- a/static-builds/README.md +++ b/static-builds/README.md @@ -21,7 +21,7 @@ python -m pip install httpx==0.19.0 python manage.py distill-local build --traceback --force --collectstatic ``` -Alternatively, you can use the [Dockerfile](./Dockerfile) in this folder. +Alternatively, you can use the [Dockerfile](/Dockerfile) and extract the build. Both output their builds to a `build/` directory. -- cgit v1.2.3 From 8b58305b50348691e8ae12b17efba828a15958a2 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Sun, 10 Oct 2021 14:54:14 +0300 Subject: Error Out On Missing Files In CI Signed-off-by: Hassan Abouelela --- .github/workflows/static-preview.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/static-preview.yaml b/.github/workflows/static-preview.yaml index 06192f02..52d7df5a 100644 --- a/.github/workflows/static-preview.yaml +++ b/.github/workflows/static-preview.yaml @@ -74,3 +74,4 @@ jobs: with: name: static-build path: docker_build/app/build/ + if-no-files-found: error -- cgit v1.2.3 From 03ad772dd15c4f8214a1b0addb3ed4f0aec666d6 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Sun, 10 Oct 2021 15:34:00 +0300 Subject: Move Static Build Into Separate Variable Signed-off-by: Hassan Abouelela --- manage.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/manage.py b/manage.py index 357134ec..90912da3 100755 --- a/manage.py +++ b/manage.py @@ -173,7 +173,9 @@ def main() -> None: # Pass any others directly to standard management commands else: - if _static_build := "distill" in sys.argv[1]: + _static_build = "distill" in sys.argv[1] + + if _static_build: # Build a static version of the site with no databases and API support os.environ["STATIC_BUILD"] = "True" if not os.getenv("PARENT_HOST"): -- cgit v1.2.3 From 036f1b6544a3f7bf8fddd3c71d42eebca784ea98 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Sun, 10 Oct 2021 20:06:19 +0300 Subject: Uses Nightly To Download Artifacts Signed-off-by: Hassan Abouelela --- static-builds/README.md | 3 ++- static-builds/netlify_build.py | 27 +++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/static-builds/README.md b/static-builds/README.md index fe24df07..b5cba896 100644 --- a/static-builds/README.md +++ b/static-builds/README.md @@ -27,6 +27,8 @@ 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: @@ -39,7 +41,6 @@ Publish Directory: Environment Variables: - PYTHON_VERSION: 3.8 -- TOKEN: A GitHub token with access to download build artifacts. Note that at this time, if you are deploying to netlify yourself, you won't have access to the diff --git a/static-builds/netlify_build.py b/static-builds/netlify_build.py index 5699c3e4..4e1e6106 100644 --- a/static-builds/netlify_build.py +++ b/static-builds/netlify_build.py @@ -3,16 +3,12 @@ # WARNING: This file must remain compatible with python 3.8 # This script performs all the actions required to build and deploy our project on netlify -# It requires the following environment variable: - -# TOKEN: A GitHub access token that can download the artifact. -# For PAT, the only scope needed is `public_repos` - # It depends on the following packages, which are set in the netlify UI: # httpx == 0.19.0 import os import time +import typing import zipfile from pathlib import Path from urllib import parse @@ -20,11 +16,16 @@ from urllib import parse import httpx API_URL = "https://api.github.com" +NIGHTLY_URL = "https://nightly.link" OWNER, REPO = parse.urlparse(os.getenv("REPOSITORY_URL")).path.lstrip("/").split("/")[0:2] -def get_build_artifact() -> str: - """Search for a build artifact, and return the download URL.""" +def get_build_artifact() -> typing.Tuple[int, str]: + """ + Search for a build artifact, and return the result. + + The return is a tuple of the check suite ID, and the URL to the artifacts. + """ print("Fetching build URL.") if os.getenv("PULL_REQUEST").lower() == "true": @@ -74,7 +75,7 @@ def get_build_artifact() -> str: else: print(f"Found artifact URL:\n{run['artifacts_url']}") - return run["artifacts_url"] + return run["check_suite_id"], run["artifacts_url"] _run = httpx.get(run["url"]) _run.raise_for_status() @@ -83,7 +84,7 @@ def get_build_artifact() -> str: raise Exception("Polled for the artifact workflow, but it was not ready in time.") -def download_artifact(url: str) -> None: +def download_artifact(suite_id: int, url: str) -> None: """Download a build artifact from `url`, and unzip the content.""" print("Fetching artifact data.") @@ -101,9 +102,8 @@ def download_artifact(url: str) -> None: else: raise Exception("Could not find an artifact with the expected name.") - zipped_content = httpx.get(artifact["archive_download_url"], headers={ - "Authorization": f"token {os.getenv('TOKEN')}" - }) + artifact_url = f"{NIGHTLY_URL}/{OWNER}/{REPO}/suites/{suite_id}/artifacts/{artifact['id']}" + zipped_content = httpx.get(artifact_url) zipped_content.raise_for_status() zip_file = Path("temp.zip") @@ -119,5 +119,4 @@ def download_artifact(url: str) -> None: if __name__ == "__main__": print("Build started") - artifact_url = get_build_artifact() - download_artifact(artifact_url) + download_artifact(*get_build_artifact()) -- cgit v1.2.3 From a7e9bdd9dbf086094dd138feb9464b7e7fe8b08b Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Sun, 10 Oct 2021 22:19:26 +0300 Subject: Adds Redirects To Static Builds Dynamically adds static HTML redirects for static builds. Signed-off-by: Hassan Abouelela --- pydis_site/apps/redirect/redirects.yaml | 2 +- pydis_site/apps/redirect/urls.py | 114 ++++++++++++++++++++++++++++---- pydis_site/urls.py | 16 +++-- 3 files changed, 112 insertions(+), 20 deletions(-) diff --git a/pydis_site/apps/redirect/redirects.yaml b/pydis_site/apps/redirect/redirects.yaml index def4154b..9bcf3afd 100644 --- a/pydis_site/apps/redirect/redirects.yaml +++ b/pydis_site/apps/redirect/redirects.yaml @@ -182,7 +182,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 = "" + + +@dataclasses.dataclass(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/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')), + ) -- cgit v1.2.3 From 455dc792d38766156747a156e14b75d28c211525 Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Sun, 17 Oct 2021 15:06:38 +0300 Subject: Restructure bot contributing guide (#591) * Restructure bot contributing guide Restructures the bot guide to focus on getting users "through the door" first, and slowly adding additional information and options. The guide is built in terms of showing ways to run the bot and how to achieve it, rather than listing all of the information for each configuration and environment variable right away. That said, the attempt was to still present the reader with the full information they need, and to still be able to use as a quick reference for contributors who are already familiar with the project. As a style choice, this guide incorporates collapsible elements, which required some CSS and JS additions. This could be resolved with the bulma-collapsible extension, but it doesn't seem to function well inside django templates. The guide includes an optional config.yml to use with the test server template. This is a temporary measure until we get the bootstrapping application going. This guide additionally splits the bot account creation and obtaining Discord ID's guides and they didn't seem related. The original file is kept for now as it is used by other guides at the moment. --- .../guides/pydis-guides/contributing/bot.md | 735 +++++++++++++++++---- .../contributing/creating-bot-account.md | 17 + .../contributing/obtaining-discord-ids.md | 42 ++ pydis_site/static/css/content/page.css | 13 + .../content/contributing/pycharm_run_module.png | Bin 0 -> 38758 bytes pydis_site/static/js/content/page.js | 13 + pydis_site/templates/content/base.html | 1 + 7 files changed, 681 insertions(+), 140 deletions(-) create mode 100644 pydis_site/apps/content/resources/guides/pydis-guides/contributing/creating-bot-account.md create mode 100644 pydis_site/apps/content/resources/guides/pydis-guides/contributing/obtaining-discord-ids.md create mode 100644 pydis_site/static/images/content/contributing/pycharm_run_module.png create mode 100644 pydis_site/static/js/content/page.js 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 7419858e..4c09165d 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 @@ -2,206 +2,661 @@ title: Contributing to Bot description: A guide to setting up and configuring Bot. icon: fab fa-github -toc: 1 +toc: 3 --- - -# Requirements -* [Python 3.9](https://www.python.org/downloads/) -* [Poetry](https://github.com/python-poetry/poetry#installation) - * `pip install poetry` -* [Git](https://git-scm.com/downloads) - * [Windows](https://git-scm.com/download/win) - * [MacOS](https://git-scm.com/download/mac) or `brew install git` - * [Linux](https://git-scm.com/download/linux) -* A running webserver for the [site](../site) - * Follow the linked guide only if you don't want to use Docker or if you plan to do development on the site project too. - -## Using Docker - -Both the site and the bot can be started using Docker. -Using Docker is generally recommended (but not strictly required) because it abstracts away some additional set up work, especially for the site. -However, if you plan to attach a debugger to either the site or the bot, run the respective project directly on your system (AKA the _host_) instead. - -The requirements for Docker are: - -* [Docker CE](https://docs.docker.com/install/) -* [Docker Compose](https://docs.docker.com/compose/install/) (This already comes bundled on macOS and Windows, so you shouldn't need to install it) - * `pip install docker-compose` +The purpose of this guide is to get you a running local version of [the Python bot](https://github.com/python-discord/bot). +This page will focus on the quickest steps one can take, with mentions of alternatives afterwards. + +### Clone The Repository +First things first, to run the bot's code and make changes to it, you need a local version of it (on your computer). + +
    + +
    +
    +

    If you don't have Git on your computer already, install it. You can additionally install a Git GUI such as GitKraken, or the GitHub CLI.

    +

    To learn more about Git, you can look into our guides, as well as this cheatsheet, Learn Git Branching, 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.

    +

    Creating a copy of a repository under your own account is called a fork. This is where all your changes and commits will be pushed to, and from where your pull requests will originate from.

    +

    Learn about forking a project.

    +
    +
    +
    +
    + +You will need to create a fork of [the project](https://github.com/python-discord/bot), and clone the fork. +Once this is done, you will have completed the first step towards having a running version of the bot. + +#### Working on the Repository Directly +If you are a member of the organisation (a member of [this list](https://github.com/orgs/python-discord/people), or in our particular case, server staff), you can clone the project repository without creating a fork, and work on a feature branch instead. --- -# Fork the project -You will need access to a copy of the git repository of your own that will allow you to edit the code and push your commits to. -Creating a copy of a repository under your own account is called a _fork_. - -* [Learn how to create a fork of the repository here.](../forking-repository) -This is where all your changes and commits will be pushed to, and from where your PRs will originate from. +### Set Up a Test Server +The Python bot is tightly coupled with the Python Discord server, so to have a functional version of the bot you need a server with channels it can use. +It's possible to set the bot to use a single channel for all cogs, but that will cause extreme spam and will be difficult to work with. -For any staff member, since you have write permissions already to the original repository, you can just create a feature branch to push your commits to instead. - ---- -# Development environment -1. [Clone your fork to a local project directory](../cloning-repository/) -2. [Install the project's dependencies](../installing-project-dependencies/) +You can start your own server and set up channels as you see fit, but for your convenience we have a template for a development server you can use: [https://discord.new/zmHtscpYN9E3](https://discord.new/zmHtscpYN9E3). +Keep in mind that this is not a mirror of the Python server, but a reduced version for testing purposes. A lot of the channels in the Python server were merged. --- -# Test server and bot account -You will need your own test server and bot account on Discord to test your changes to the bot. -* [**Create a test server**](../setting-test-server-and-bot-account#setting-up-a-test-server) -* [**Create a bot account**](../setting-test-server-and-bot-account#setting-up-a-bot-account) -* Invite it to the server you just created. +### Set Up a Bot Account +You will need your own bot account on Discord to test your changes to the bot. +See [here](../creating-bot-account) for help with setting up a bot account. Once you have a bot account, invite it to the test server you created in the previous section. -### Privileged Intents +#### Privileged Intents -With `discord.py` 1.5 and later, it is now necessary to explicitly request that your Discord bot receives certain gateway events. +It is necessary to explicitly request that your Discord bot receives certain gateway events. The Python bot requires the `Server Member Intent` to function. In order to enable it, visit the [Developer Portal](https://discord.com/developers/applications/) (from where you copied your bot's login token) and scroll down to the `Privileged Gateway Intents` section. The `Presence Intent` is not necessary and can be left disabled. If your bot fails to start with a `PrivilegedIntentsRequired` exception, this indicates that the required intent was not enabled. -### Server Setup - -Setup categories, channels, emojis, roles, and webhooks in your server. To see what needs to be added, please refer to the following sections in the `config-default.yml` file: +--- -* `style.emojis` +### Configure the Bot +You now have both the bot's code and a server to run it on. It's time you to connect the two by changing the bot's configurations. + +#### config.yml +Entering the directory of the cloned code, you will find a file named `config-default.yml`. +This file contains the various configurations we use to make the bot run on the Python Discord server, such as channel and role IDs, and the emojis it works with. +It also contains configurations such as how long it takes for a help channel to time out, and how many messages a user needs to voice-verify. + +To run the bot in your test server, you will need to override some of those configurations. +Create and open a new file in the directory called `config.yml`. Alternatively, copy the `config-default.yml` file and rename the copy to `config.yml`. +The bot will first look at the items in `config.yml`, and will fall back to `config-default.yml` only if necessary. Note that you don't have to specify all items in `config.yml`, just the ones you want to override such as channel IDs. + +See [here](../obtaining-discord-ids) for help with obtaining Discord IDs. + +
    + +
    +
    +

    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 # symbols, and replace the characters with the right IDs.

    +
    +                
    +bot:
    +    prefix:      "!"
    +
    +    redis:
    +        host:  "redis"
    +        password: null
    +        port:  6379
    +        use_fakeredis: true
    +
    +    stats:
    +        presence_update_timeout: 300
    +        statsd_host: "graphite.default.svc.cluster.local"
    +
    +urls:
    +    # PyDis site vars
    +    site:        &DOMAIN       "web:8000"
    +    site_api:    &API    !JOIN [*DOMAIN, "/api"]
    +    site_api_schema:           "http://"
    +    site_paste:  &PASTE  !JOIN ["paste.", "pythondiscord.com"]
    +    site_schema: &SCHEMA       "http://"
    +    site_staff:  &STAFF  !JOIN [*DOMAIN, "/staff"]
    +
    +    paste_service:                      !JOIN ["https://", *PASTE, "/{key}"]
    +    site_logs_view:                     !JOIN [*SCHEMA, *STAFF, "/bot/logs"]
    +
    +    # Snekbox
    +    snekbox_eval_api: "http://localhost:8060/eval"
    +
    +##### <<  Replace the following � characters with the channel IDs in your test server  >> #####
    +#  This assumes the template was used: https://discord.new/zmHtscpYN9E3
    +dev_guild:
    +    id: &DEV_GUILD_ID   �
    +
    +    categories:
    +        logs:           &DEV_LOGS               �
    +        help_available: &DEV_HELP_AVAILABLE     �
    +        help_occupied:  &DEV_HELP_OCCUPIED      �
    +        help_dormant:   &DEV_HELP_DORMANT       �
    +        voice:          &DEV_VOICE              �
    +
    +    channels:
    +        # Staff
    +        admins_mods:            &DEV_ADMINS_MODS            �
    +        lounge_helpers_org:     &DEV_LOUNGE_HELPERS_ORG     �
    +        defcon:                 &DEV_DEFCON                 �
    +        incidents:              &DEV_INCIDENTS              �
    +        incidents_archive:      &DEV_INCIDENTS_ARCHIVE      �
    +        staff_announcements:    &DEV_STAFF_ANNOUNCEMENTS    �
    +        dev_logs:               &DEV_DEV_LOGS               �
    +
    +        # Logs
    +        all_logs:   &DEV_ALL_LOGS   �
    +        bb_logs:    &DEV_BB_LOGS    �
    +        duck_pond:  &DEV_DUCK_POND  �
    +
    +        # Available Help Channels
    +        how_to_get_help:    &DEV_HTGH   �
    +
    +        # Miscellaneous
    +        bot_commands:       &DEV_BOT_CMD    �
    +        general_meta_voice: &DEV_GMV        �
    +        dev_core_contrib:   &DEV_DEV        �
    +
    +        # Voice
    +        voice-verification: &DEV_VOICE_VER      �
    +        vc:                 &DEV_VC             �
    +        staff_voice:        &DEV_STAFF_VOICE    �
    +
    +        # News
    +        announcements:  &DEV_ANNOUNCEMENTS  �
    +        py_news:        &DEV_PY_NEWS        �
    +
    +        # Off-topic
    +        off_topic_0: &DEV_OT_0  �
    +        off_topic_1: &DEV_OT_1  �
    +        off_topic_2: &DEV_OT_2  �
    +
    +guild:
    +    ##### <<  Replace the following � characters with the role and webhook IDs in your test server  >> #####
    +    roles:
    +        announcements:                          �
    +        contributors:                           �
    +        help_cooldown:                          �
    +        muted:              &MUTED_ROLE         �
    +        partners:           &PY_PARTNER_ROLE    �
    +        python_community:   &PY_COMMUNITY_ROLE  �
    +        voice_verified:                         �
    +
    +        # Staff
    +        admins:             &ADMINS_ROLE    �
    +        core_developers:                    �
    +        devops:                             �
    +        domain_leads:                       �
    +        helpers:            &HELPERS_ROLE   �
    +        moderators:         &MODS_ROLE      �
    +        mod_team:           &MOD_TEAM_ROLE  �
    +        owners:             &OWNERS_ROLE    �
    +        code_jam_event_team:                �
    +        project_leads:                      �
    +
    +        # Code Jam
    +        team_leaders:   �
    +
    +        # Streaming
    +        video: �
    +
    +    webhooks:
    +        big_brother:                            �
    +        dev_log:                                �
    +        duck_pond:                              �
    +        incidents_archive:                      �
    +        python_news:        &PYNEWS_WEBHOOK     �
    +        talent_pool:                            �
    +
    +    ##### <<  At this point your test bot should be able to mostly work with your test server  >> #####
    +    #  The following is the actual configs the bot uses, don't delete these.
    +    id: *DEV_GUILD_ID
    +    invite: "https://discord.gg/python"
    +
    +    categories:
    +        help_available:                     *DEV_HELP_AVAILABLE
    +        help_dormant:                       *DEV_HELP_DORMANT
    +        help_in_use:                        *DEV_HELP_OCCUPIED
    +        logs:                               *DEV_LOGS
    +        voice:                              *DEV_VOICE
    +
    +    channels:
    +        # Public announcement and news channels
    +        announcements:  *DEV_ANNOUNCEMENTS
    +        change_log:     *DEV_ANNOUNCEMENTS
    +        mailing_lists:  *DEV_ANNOUNCEMENTS
    +        python_events:  *DEV_ANNOUNCEMENTS
    +        python_news:    *DEV_PY_NEWS
    +
    +        # Development
    +        dev_contrib:        *DEV_DEV
    +        dev_core:           *DEV_DEV
    +        dev_log:            *DEV_DEV_LOGS
    +
    +        # Discussion
    +        meta:                               *DEV_GMV
    +        python_general:     *DEV_GMV
    +
    +        # Python Help: Available
    +        cooldown:           *DEV_HTGH
    +        how_to_get_help:    *DEV_HTGH
    +
    +        # Topical
    +        discord_py:         *DEV_GMV
    +
    +        # Logs
    +        attachment_log:     *DEV_ALL_LOGS
    +        message_log:        *DEV_ALL_LOGS
    +        mod_log:            *DEV_ALL_LOGS
    +        user_log:           *DEV_ALL_LOGS
    +        voice_log:          *DEV_ALL_LOGS
    +
    +        # Off-topic
    +        off_topic_0:    *DEV_OT_0
    +        off_topic_1:    *DEV_OT_1
    +        off_topic_2:    *DEV_OT_2
    +
    +        # Special
    +        bot_commands:       *DEV_BOT_CMD
    +        voice_gate:         *DEV_VOICE_VER
    +        code_jam_planning:  *DEV_ADMINS_MODS
    +
    +        # Staff
    +        admins:             *DEV_ADMINS_MODS
    +        admin_spam:         *DEV_ADMINS_MODS
    +        defcon:             *DEV_DEFCON
    +        duck_pond:          *DEV_DUCK_POND
    +        helpers:            *DEV_LOUNGE_HELPERS_ORG
    +        incidents:                          *DEV_INCIDENTS
    +        incidents_archive:                  *DEV_INCIDENTS_ARCHIVE
    +        mods:               *DEV_ADMINS_MODS
    +        mod_alerts:                         *DEV_ADMINS_MODS
    +        mod_meta:           *DEV_ADMINS_MODS
    +        mod_spam:           *DEV_ADMINS_MODS
    +        mod_tools:          *DEV_ADMINS_MODS
    +        organisation:       *DEV_LOUNGE_HELPERS_ORG
    +        staff_lounge:       *DEV_LOUNGE_HELPERS_ORG
    +
    +        # Staff announcement channels
    +        admin_announcements:    *DEV_STAFF_ANNOUNCEMENTS
    +        mod_announcements:      *DEV_STAFF_ANNOUNCEMENTS
    +        staff_announcements:    *DEV_STAFF_ANNOUNCEMENTS
    +
    +        # Voice Channels
    +        admins_voice:       *DEV_STAFF_VOICE
    +        code_help_voice_1:  *DEV_VC
    +        code_help_voice_2:  *DEV_VC
    +        general_voice:      *DEV_VC
    +        staff_voice:        *DEV_STAFF_VOICE
    +
    +        # Voice Chat
    +        code_help_chat_1:                   *DEV_GMV
    +        code_help_chat_2:                   *DEV_GMV
    +        staff_voice_chat:                   *DEV_ADMINS_MODS
    +        voice_chat:                         *DEV_GMV
    +
    +        # Watch
    +        big_brother_logs:                   *DEV_BB_LOGS
    +
    +    moderation_categories:
    +        - *DEV_LOGS
    +
    +    moderation_channels:
    +        - *DEV_ADMINS_MODS
    +
    +    # Modlog cog ignores events which occur in these channels
    +    modlog_blacklist:
    +        - *DEV_ADMINS_MODS
    +        - *DEV_ALL_LOGS
    +        - *DEV_STAFF_VOICE
    +
    +    reminder_whitelist:
    +        - *DEV_BOT_CMD
    +        - *DEV_DEV
    +
    +    moderation_roles:
    +        - *ADMINS_ROLE
    +        - *MODS_ROLE
    +        - *MOD_TEAM_ROLE
    +        - *OWNERS_ROLE
    +
    +    staff_roles:
    +        - *ADMINS_ROLE
    +        - *HELPERS_ROLE
    +        - *MODS_ROLE
    +        - *OWNERS_ROLE
    +
    +##### <<  The bot shouldn't fail without these, but commands adding specific emojis won't work.  >> #####
    +#  You should at least set the trashcan. Set the incidents emojis if relevant.
    +style:
    +    emojis:
    +        badge_bug_hunter: "<:bug_hunter_lvl1:�>"
    +        badge_bug_hunter_level_2: "<:bug_hunter_lvl2:�>"
    +        badge_early_supporter: "<:early_supporter:�>"
    +        badge_hypesquad: "<:hypesquad_events:�>"
    +        badge_hypesquad_balance: "<:hypesquad_balance:�>"
    +        badge_hypesquad_bravery: "<:hypesquad_bravery:�>"
    +        badge_hypesquad_brilliance: "<:hypesquad_brilliance:�>"
    +        badge_partner: "<:partner:�>"
    +        badge_staff: "<:discord_staff:�>"
    +        badge_verified_bot_developer: "<:verified_bot_dev:�>"
    +
    +        defcon_shutdown:    "<:defcondisabled:�>"
    +        defcon_unshutdown:  "<:defconenabled:�>"
    +        defcon_update:      "<:defconsettingsupdated:�>"
    +
    +        failmail: "<:failmail:�>"
    +
    +        #incident_actioned:      "<:incident_actioned:�>"
    +        incident_investigating: "<:incident_investigating:�>"
    +        incident_unactioned:    "<:incident_unactioned:�>"
    +
    +        status_dnd:     "<:status_dnd:�>"
    +        status_idle:    "<:status_idle:�>"
    +        status_offline: "<:status_offline:�>"
    +        status_online:  "<:status_online:�>"
    +
    +        trashcan: "<:trashcan:�>"
    +
    +##### <<  Optional - If you don't care about the filtering and help channel cogs, ignore the rest of this file  >> #####
    +filter:
    +    # What do we filter?
    +    filter_domains:        true
    +    filter_everyone_ping:  true
    +    filter_invites:        true
    +    filter_zalgo:          false
    +    watch_regex:           true
    +    watch_rich_embeds:     true
    +
    +    # Notify user on filter?
    +    # Notifications are not expected for "watchlist" type filters
    +    notify_user_domains:        false
    +    notify_user_everyone_ping:  true
    +    notify_user_invites:        true
    +    notify_user_zalgo:          false
    +
    +    # Filter configuration
    +    offensive_msg_delete_days: 7     # How many days before deleting an offensive message?
    +    ping_everyone:             true
    +
    +    # Censor doesn't apply to these
    +    channel_whitelist:
    +        - *DEV_ADMINS_MODS
    +        - *DEV_BB_LOGS
    +        - *DEV_ALL_LOGS
    +        - *DEV_LOUNGE_HELPERS_ORG
    +
    +    role_whitelist:
    +        - *ADMINS_ROLE
    +        - *HELPERS_ROLE
    +        - *MODS_ROLE
    +        - *OWNERS_ROLE
    +        - *PY_COMMUNITY_ROLE
    +        - *PY_PARTNER_ROLE
    +
    +help_channels:
    +    enable: true
    +
    +    # Minimum interval before allowing a certain user to claim a new help channel
    +    claim_minutes: 1
    +
    +    # Roles which are allowed to use the command which makes channels dormant
    +    cmd_whitelist:
    +        - *HELPERS_ROLE
    +
    +    # Allowed duration of inactivity before making a channel dormant
    +    idle_minutes: 1
    +
    +    # Allowed duration of inactivity when channel is empty (due to deleted messages)
    +    # before message making a channel dormant
    +    deleted_idle_minutes: 1
    +
    +    # Maximum number of channels to put in the available category
    +    max_available: 2
    +
    +    # Maximum number of channels across all 3 categories
    +    # Note Discord has a hard limit of 50 channels per category, so this shouldn't be > 50
    +    max_total_channels: 20
    +
    +    # Prefix for help channel names
    +    name_prefix: 'help-'
    +
    +    # Notify if more available channels are needed but there are no more dormant ones
    +    notify: true
    +
    +    # Channel in which to send notifications
    +    notify_channel: *DEV_LOUNGE_HELPERS_ORG
    +
    +    # Minimum interval between helper notifications
    +    notify_minutes: 5
    +
    +    # Mention these roles in notifications
    +    notify_roles:
    +        - *HELPERS_ROLE
    +
    +##### <<  Add any additional sections you need to override from config-default.yml  >> #####
    +            
    +          
    +
    +
    + +If you don't wish to use the provided `config.yml` above, these are the main sections in `config-default.yml` that need overriding: + +* `guild.id` * `guild.categories` * `guild.channels` * `guild.roles` * `guild.webhooks` +* `style.emojis` -We understand this is tedious and are working on a better solution for setting up test servers. -In the meantime, [here](https://discord.new/zmHtscpYN9E3) is a template for you to use.
    +Additionally: ---- -# Configure the bot -You will need to copy IDs of the test Discord server, as well as the created channels and roles to paste in the config file. -If you're not sure how to do this, [check out the information over here.](../setting-test-server-and-bot-account#obtain-the-ids) - -1. Create a copy of `config-default.yml` named `config.yml` in the same directory. -2. Set `guild.id` to your test servers's ID. -3. Change the IDs in the [sections](#server-setup) mentioned earlier to match the ones in your test server. -4. Set `urls.site_schema` and `urls.site_api_schema` to `"http://"`. -5. Set `urls.site`: - - If running the webserver in Docker, set it to `"web:8000"`. - - If the site container is running separately (i.e. started from a clone of the site repository), then [COMPOSE_PROJECT_NAME](../docker/#compose-project-names) has to be set to use this domain. If you choose not to set it, the domain in the following step can be used instead. - - If running the webserver locally and the hosts file has been configured, set it to `"pythondiscord.local:8000"`. - - Otherwise, use whatever domain corresponds to the server where the site is being hosted. -6. Set `urls.site_api` to whatever value you assigned to `urls.site` with `api` prefixed to it, for example if you set `urls.site` to `web:8000` then set `urls.site_api` to `api.web:8000`. -7. Setup the environment variables listed in the section below. - -### Environment variables - -These contain various settings used by the bot. -To learn how to set environment variables, read [this page](../configure-environment-variables) first. +* At this stage, set `bot.redis.use_fakeredis` to `true`. If you're looking for instructions for working with Redis, see [Working with Redis](#optional-working-with-redis). +* Set `urls.site_api` to `!JOIN [*DOMAIN, "/api]`. +* Set `urls.site_schema` and `urls.site_api_schema` to `"http://"`. -The following is a list of all available environment variables used by the bot: +We understand this is tedious and are working on a better solution for setting up test servers. -| Variable | Required | Description | -| -------- | -------- | -------- | -| `BOT_TOKEN` | Always | Your Discord bot account's token (see [Test server and bot account](#test-server-and-bot-account)). | -| `BOT_API_KEY` | When running bot without Docker | Used to authenticate with the site's API. When using Docker to run the bot, this is automatically set. By default, the site will always have the API key shown in the example below. | -| `BOT_SENTRY_DSN` | When connecting the bot to sentry | The DSN of the sentry monitor. | -| `BOT_TRACE_LOGGERS ` | When you wish to see specific or all trace logs | Comma separated list that specifies which loggers emit trace logs through the listed names. If the ! prefix is used, all of the loggers except the listed ones are set to the trace level. If * is used, the root logger is set to the trace level. | -| `REDIS_PASSWORD` | When not using FakeRedis | The password to connect to the redis database. *Leave empty if you're not using REDIS.* | +
    + +
    +
    + While it's technically possible to edit config-default.yml 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. + In contrast, the config.yml file can remain in your local copy of the code, and will be ignored by commits via the project's .gitignore. +
    +
    +
    +
    + +#### .env +The second file you need to create is the one containing the environment variables, and needs to be named `.env`. +Inside, add the line `BOT_TOKEN=YourDiscordBotTokenHere`. See [here](../creating-bot-account) for help with obtaining the bot token. + +The `.env` file will be ignored by commits. --- -If you are running on the host, while not required, we advise you set `use_fakeredis` to `true` in your `config.yml` file during development to avoid the need of setting up a Redis server. -It does mean you may lose persistent data on restart but this is non-critical. -Otherwise, you should set up a Redis instance and fill in the necessary config. -{: .notification .is-warning } +### Run it! +#### With Docker +You are now almost ready to run the Python bot. The simplest way to do so is with Docker. + +
    + +
    +
    + The requirements for Docker are: +
      +
    • Docker CE
    • +
    • Docker Compose. If you're using macOS and Windows, this already comes bundled with the previous installation. Otherwise, you can download it either from the website, or by running pip install docker-compose.
    • +
    +

    If you get any Docker related errors, reference the Possible Issue section of the Docker page.

    +
    +
    +
    +
    + +In your `config.yml` file: + +* Set `urls.site` to `"web:8000"`. +* If you wish to work with snekbox set `urls.snekbox_eval_api` to `"http://snekbox:8060/eval"`. + +Assuming you have Docker installed **and running**, enter the cloned repo in the command line and type `docker-compose up`. + +After pulling the images and building the containers, your bot will start. Enter your server and type `!help` (or whatever prefix you chose instead of `!`). + +Your bot is now running, but this method makes debugging with an IDE a fairly involved process. For additional running methods, continue reading the following sections. + +#### With the Bot Running Locally +The advantage of this method is that you can run the bot's code in your preferred editor, with debugger and all, while keeping all the setup of the bot's various dependencies inside Docker. + +* Append the following line to your `.env` file: `BOT_API_KEY=badbot13m0n8f570f942013fc818f234916ca531`. +* In your `config.yml` file, set `urls.site` to `"localhost:8000"`. If you wish to keep using `web:8000`, then [COMPOSE_PROJECT_NAME](../docker/#compose-project-names) has to be set. +* To work with snekbox, set `urls.snekbox_eval_api` to `"http://localhost:8060/eval"` + +You will need to start the services separately, but if you got the previous section with Docker working, that's pretty simple: + +* `docker-compose up web` to start the site container. This is required. +* `docker-compose up snekbox` to start the snekbox container. You only need this if you're planning on working on the snekbox cog. +* `docker-compose up redis` to start the Redis container. You only need this if you're not using fakeredis. For more info refer to [Working with Redis](#optional-working-with-redis). + +You can start several services together: `docker-compose up web snekbox redis`. + +##### Setting Up a Development Environment +The bot's code is Python code like any other. To run it locally, you will need the right version of Python with the necessary packages installed: + +1. Make sure you have [Python 3.9](https://www.python.org/downloads/) installed. It helps if it is your system's default Python version. +2. [Install Poetry](https://github.com/python-poetry/poetry#installation). +3. [Install the dependencies](../installing-project-dependencies). + +With at least the site running in Docker already (see the previous section on how to start services separately), you can now start the bot locally through the command line, or through your preferred IDE. +
    + +
    +
    + Notice that the bot is started as a module. There are several ways to do so: +
      +
    • Through the command line, inside the bot directory, with either poetry run task start, or directly python -m bot.
    • +
    • If using PyCharm, enter Edit Configurations and set everything according to this image:
    • +
    • If using Visual Studio Code, set the interpreter to the poetry environment you created. In launch.json create a new Python configuration, and set the name of the program to be run to bot. VSC will correctly run it as a module.
    • +
    +
    +
    +
    +
    + +#### With More Things Running Locally +You can run additional services on the host, but this guide won't go over how to install and start them in this way. +If possible, prefer to start the services through Docker to replicate the production environment as much as possible. + +The site, however, is a mandatory service for the bot. +Refer to the [previous section](#with-the-bot-running-locally) and the [site contributing guide](../site) to learn how to start it on the host, in which case you will need to change `urls.site` in `config.yml` to wherever the site is being hosted. --- +### Development Tips +Now that you have everything setup, it is finally time to make changes to the bot! -Example `.env` file: +#### Working with Git -```shell -BOT_TOKEN=YourDiscordBotTokenHere -BOT_API_KEY=badbot13m0n8f570f942013fc818f234916ca531 -REDDIT_CLIENT_ID=YourRedditClientIDHere -REDDIT_SECRET=YourRedditSecretHere -``` +If you have not yet [read the contributing guidelines](../contributing-guidelines), now is a good time. +Contributions that do not adhere to the guidelines may be rejected. ---- -# Run the project +Notably, version control of our projects is done using Git and Github. +It can be intimidating at first, so feel free to ask for any help in the server. -The bot can run with or without Docker. -When using Docker, the site, which is a prerequisite, can be automatically set up too. -If you don't use Docker, you have to first follow [the site guide](../site/) to set it up yourself. -The bot and site can be started using independent methods. -For example, the site could run with Docker and the bot could run directly on your system (AKA the _host_) or vice versa. +[**Click here to see the basic Git workflow when contributing to one of our projects.**](../working-with-git/) -## Run with Docker +#### Running tests -The following sections describe how to start either the site, bot, or both using Docker. -If you are not interested in using Docker, see [this page](../site/) for setting up the site and [this section](#run-on-the-host) for running the bot. +[This section](https://github.com/python-discord/bot/blob/main/tests/README.md#tools) of the README in the `tests` repository will explain how to run tests. +The whole document explains how unittesting works, and how it fits in the context of our project. -If you get any Docker related errors, reference the [Possible Issues](../docker#possible-issues) section of the Docker page. +Make sure to run tests *before* pushing code. -### Site and bot +Even if you run the bot through Docker, you might want to [setup a development environment](#setting-up-a-development-environment) in order to run the tests locally. -This method will start both the site and the bot using Docker. +#### Lint before you push +As mentioned in the [contributing guidelines](../contributing-guidelines), you should make sure your code passes linting for each commit you make. -Start the containers using Docker Compose while inside the root of the project directory: +For ease of development, you can install the pre-commit hook with `poetry run task precommit`, which will check your code every time you try to commit it. +For that purpose, even if you run the bot through Docker, you might want to [setup a development environment](#setting-up-a-development-environment), as otherwise the hook installation will fail. -```shell -docker-compose up -``` +#### Reloading parts of the bot +If you make changes to an extension, you might not need to restart the entire bot for the changes to take effect. The command `!ext reload ` re-imports the files associated with the extension. +Invoke `!ext list` for a full list of the available extensions. In this bot in particular, cogs are defined inside extensions. -The `-d` option can be appended to the command to run in detached mode. -This runs the containers in the background so the current terminal session is available for use with other things. +Note that if you changed code that is not associated with a particular extension, such as utilities, converters, and constants, you will need to restart the bot. -### Site only +#### Adding new statistics -This method will start only the site using Docker. +Details on how to add new statistics can be found on the [statistic infrastructure page](https://blog.pythondiscord.com/statistics-infrastructure). +We are always open to more statistics so add as many as you can! -```shell -docker-compose up site -``` +--- -See [this section](#run-on-the-host) for how to start the bot on the host. +### Optional: Working with Redis +In [Configure the Bot](#configyml) you were asked to set `bot.redis.use_fakeredis` to `true`. If you do not need to work on features that rely on Redis, this is enough. Fakeredis will give the illusion that features relying on Redis are saving information properly, but restarting the bot or the specific cog will wipe that information. -### Bot only +If you are working on a feature that relies on Redis, you will need to enable Redis to make sure persistency is achieved for the feature across restarts. The first step towards that is going to `config.yml` and setting `bot.redis.use_fakeredis` to `false`. -This method will start only the bot using Docker. -The site has to have been started somehow beforehand. +#### Starting Redis in Docker (Recommended) +If you're using the Docker image provided in the project's Docker Compose, open your `config.yml` file, set `bot.redis.host` to `redis`, and `bot.redis.password` to `null`. -Start the bot using Docker Compose while inside the root of the project directory: +#### Starting Redis Using Other Methods +You can run your own instance of Redis, but in that case you will need to correctly set `bot.redis.host` and `bot.redis.port`, and the `bot.redis.password` value in `config-default.yml` should not be overridden. Then, enter the `.env` file, and set `REDIS_PASSWORD` to whatever password you set. -```shell -docker-compose up --no-deps bot -``` +--- -## Run on the host +### Optional: Working with Metricity +[Metricity](https://github.com/python-discord/metricity) is our home-grown bot for collecting metrics on activity within the server, such as what users are present, and IDs of the messages they've sent. +Certain features in the Python bot rely on querying the Metricity database for information such as the number of messages a user has sent, most notably the voice verification system. -Running on the host is particularly useful if you wish to debug the bot. -The site has to have been started somehow beforehand. +If you wish to work on a feature that relies on Metricity, for your convenience we've made the process of using it relatively painless with Docker: Enter the `.env` file you've written for the Python bot, and append the line `USE_METRICITY=true`. +Note that if you don't need Metricity, there's no reason to have it enabled as it is just unnecessary overhead. -```shell -poetry run task start +To make the Metricity bot work with your test server, you will need to override its configurations similarly to the Python bot. +You can see the various configurations in [the Metricity repo](https://github.com/python-discord/metricity), but the bare minimum is the guild ID setting. +In your local version of the Python bot repo, create a file called `metricity-config.toml` and insert the following lines: +```yaml +[bot] +guild_id = replace_with_your_guild_id ``` +To properly replicate production behavior, set the `staff_role_id`, `staff_categories`, and `ignore_categories` fields as well. ---- -## Working with Git -Now that you have everything setup, it is finally time to make changes to the bot! -If you have not yet [read the contributing guidelines](../contributing-guidelines), now is a good time. -Contributions that do not adhere to the guidelines may be rejected. +Now, `docker-compose up` will also start Metricity. -Notably, version control of our projects is done using Git and Github. -It can be intimidating at first, so feel free to ask for any help in the server. +If you want to run the bot locally, you can run `docker-compose up metricity` instead. -[**Click here to see the basic Git workflow when contributing to one of our projects.**](../working-with-git/) +--- -## Adding new statistics +### Issues? +If you have any issues with setting up the bot, come discuss it with us on the [#dev-contrib](https://discord.gg/2h3qBv8Xaa) channel on our server. -Details on how to add new statistics can be found on the [statistic infrastructure page](https://blog.pythondiscord.com/statistics-infrastructure). -We are always open to more statistics so add as many as you can! +If you find any bugs in the bot or would like to request a feature, feel free to open an issue on the repository. -## Running tests +--- -[This section](https://github.com/python-discord/bot/blob/main/tests/README.md#tools) of the README in the `tests` repository will explain how to run tests. -The whole document explains how unittesting works, and how it fits in the context of our project. +### Appendix: Full ENV File Options +The following is a list of all available environment variables used by the bot: + +| Variable | Required | Description | +| -------- | -------- | -------- | +| `BOT_TOKEN` | Always | Your Discord bot account's token (see [Set Up a Bot Account](##set-up-a-bot-account)). | +| `BOT_API_KEY` | When running bot without Docker | Used to authenticate with the site's API. When using Docker to run the bot, this is automatically set. By default, the site will always have the API key shown in the example below. | +| `BOT_SENTRY_DSN` | When connecting the bot to sentry | The DSN of the sentry monitor. | +| `BOT_TRACE_LOGGERS ` | When you wish to see specific or all trace logs | Comma separated list that specifies which loggers emit trace logs through the listed names. If the ! prefix is used, all of the loggers except the listed ones are set to the trace level. If * is used, the root logger is set to the trace level. | +| `BOT_DEBUG` | In production | `true` or `false`, depending on whether to enable debug mode, affecting the behavior of certain features. `true` by default. +| `REDIS_PASSWORD` | When not using FakeRedis | The password to connect to the Redis database (see [Optional: Working with Redis](#optional-working-with-redis)). | +| `USE_METRICITY` | When using Metricity | `true` or `false`, depending on whether to enable metrics collection using Metricity (see [Optional: Working with Metricity](#optional-working-with-metricity)). `false` by default. | +| `GITHUB_API_KEY` | When you wish to interact with GitHub | The API key to interact with GitHub, for example to download files for the branding manager. +| `METABASE_USERNAME` | When you wish to interact with Metabase | The username for a Metabase admin account. +| `METABASE_PASSWORD` | When you wish to interact with Metabase | The password for a Metabase admin account. Have fun! diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/creating-bot-account.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/creating-bot-account.md new file mode 100644 index 00000000..ee38baa3 --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/creating-bot-account.md @@ -0,0 +1,17 @@ +--- +title: Setting up a Bot Account +description: How to set up a bot account. +icon: fab fa-discord +--- +1. Go to the [Discord Developers Portal](https://discordapp.com/developers/applications/). +2. Click on the `New Application` button, enter your desired bot name, and click `Create`. +3. In your new application, go to the `Bot` tab, click `Add Bot`, and confirm `Yes, do it!` +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. If you do 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**. +7. Replace `` 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=&permissions=8&scope=bot +``` +Optionally, you can generate your own invite url in the `OAuth` tab, after selecting `bot` as the scope. diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/obtaining-discord-ids.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/obtaining-discord-ids.md new file mode 100644 index 00000000..afa07b5a --- /dev/null +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/obtaining-discord-ids.md @@ -0,0 +1,42 @@ +--- +title: Obtaining Discord IDs +description: How to obtain Discord IDs to set up the bots. +icon: fab fa-discord +--- +First, enable developer mode in your client so you can easily copy IDs. + +1. Go to your `User Settings` and click on the `Appearance` tab. +2. Under `Advanced`, enable `Developer Mode`. + +#### Guild ID + +Right click the server icon and click `Copy ID`. + +#### Channel ID + +Right click a channel name and click `Copy ID`. + +#### Role ID + +Right click a role and click `Copy ID`. +The easiest way to do this is by going to the role list in the guild's settings. + +#### Emoji ID + +Insert the emoji into the Discord text box, then add a backslash (`\`) right before the emoji and send the message. +The result should be similar to the following + +```plaintext +<:bbmessage:511950877733552138> +``` + +The long number you see, in this case `511950877733552138`, is the emoji's ID. + +#### Webhook ID + +Once a [webhook](https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks) is created, the ID is found in the penultimate part of the URL. +For example, in the following URL, `661995360146817053` is the ID of the webhook. + +```plaintext +https://discordapp.com/api/webhooks/661995360146817053/t-9mI2VehOGcPuPS_F8R-6mB258Ob6K7ifhtoxerCvWyM9VEQug-anUr4hCHzdbhzfbz +``` diff --git a/pydis_site/static/css/content/page.css b/pydis_site/static/css/content/page.css index d831f86d..2d4bd325 100644 --- a/pydis_site/static/css/content/page.css +++ b/pydis_site/static/css/content/page.css @@ -77,3 +77,16 @@ 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/images/content/contributing/pycharm_run_module.png b/pydis_site/static/images/content/contributing/pycharm_run_module.png new file mode 100644 index 00000000..c5030519 Binary files /dev/null and b/pydis_site/static/images/content/contributing/pycharm_run_module.png differ diff --git a/pydis_site/static/js/content/page.js b/pydis_site/static/js/content/page.js new file mode 100644 index 00000000..366a033c --- /dev/null +++ b/pydis_site/static/js/content/page.js @@ -0,0 +1,13 @@ +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/templates/content/base.html b/pydis_site/templates/content/base.html index 21895479..00f4fce4 100644 --- a/pydis_site/templates/content/base.html +++ b/pydis_site/templates/content/base.html @@ -7,6 +7,7 @@ + {% endblock %} {% block content %} -- cgit v1.2.3 From 827337292544cb331bc7f1f70760a2d564e9cd9f Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Sun, 17 Oct 2021 15:21:28 +0300 Subject: Fix typo in bot guide --- .../apps/content/resources/guides/pydis-guides/contributing/bot.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4c09165d..27c67e37 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 @@ -444,7 +444,7 @@ If you don't wish to use the provided `config.yml` above, these are the main sec Additionally: * At this stage, set `bot.redis.use_fakeredis` to `true`. If you're looking for instructions for working with Redis, see [Working with Redis](#optional-working-with-redis). -* Set `urls.site_api` to `!JOIN [*DOMAIN, "/api]`. +* Set `urls.site_api` to `!JOIN [*DOMAIN, "/api"]`. * Set `urls.site_schema` and `urls.site_api_schema` to `"http://"`. We understand this is tedious and are working on a better solution for setting up test servers. -- cgit v1.2.3 From 29425c2466a5d6fb5f8c332aff5a0a3a61ced677 Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Sun, 17 Oct 2021 15:55:32 +0300 Subject: Remove stray `#` in bot guide --- .../apps/content/resources/guides/pydis-guides/contributing/bot.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 27c67e37..183f1505 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 @@ -648,7 +648,7 @@ The following is a list of all available environment variables used by the bot: | Variable | Required | Description | | -------- | -------- | -------- | -| `BOT_TOKEN` | Always | Your Discord bot account's token (see [Set Up a Bot Account](##set-up-a-bot-account)). | +| `BOT_TOKEN` | Always | Your Discord bot account's token (see [Set Up a Bot Account](#set-up-a-bot-account)). | | `BOT_API_KEY` | When running bot without Docker | Used to authenticate with the site's API. When using Docker to run the bot, this is automatically set. By default, the site will always have the API key shown in the example below. | | `BOT_SENTRY_DSN` | When connecting the bot to sentry | The DSN of the sentry monitor. | | `BOT_TRACE_LOGGERS ` | When you wish to see specific or all trace logs | Comma separated list that specifies which loggers emit trace logs through the listed names. If the ! prefix is used, all of the loggers except the listed ones are set to the trace level. If * is used, the root logger is set to the trace level. | -- cgit v1.2.3 From 594312fd90bf612895ca4bf4ae3c52186831d224 Mon Sep 17 00:00:00 2001 From: Hedy Li Date: Sun, 17 Oct 2021 21:13:24 +0800 Subject: FAQ: Update reference to #dev-branding Ref: https://discord.com/channels/267624335836053506/429409067623251969/896753201645232169 I think it should be updated so newer members won't get confused when they don't see a #media-branding. --- pydis_site/apps/content/resources/frequently-asked-questions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/apps/content/resources/frequently-asked-questions.md b/pydis_site/apps/content/resources/frequently-asked-questions.md index a9a092fe..1c9c3f6d 100644 --- a/pydis_site/apps/content/resources/frequently-asked-questions.md +++ b/pydis_site/apps/content/resources/frequently-asked-questions.md @@ -119,7 +119,7 @@ You can also open an issue on our meta repo on GitHub, which can be found [here] While we love our blurple Python logo, we also enjoy celebrating other events throughout the year, like Advent of Code, Pride Month, Black History Month, Valentine's Day, Diwali, and more! In the spirit of those celebrations, we like to have some fun and change our icon instead. If you're wondering why it's changed this time, check out `#changelog` on the server, as the reasoning for the recent change will be there. -If you'd like to contribute and create a Python Discord server icon for us to use, check out [our branding repo](https://github.com/python-discord/branding) for what we currently have and talk to us in the `#media-branding` channel in the server. +If you'd like to contribute and create a Python Discord server icon for us to use, check out [our branding repo](https://github.com/python-discord/branding) for what we currently have and talk to us in the `#dev-branding` channel in the server. ## Misc -- cgit v1.2.3 From 569c2c2d2540fda797ade699c7acb67e402114e5 Mon Sep 17 00:00:00 2001 From: Hedy Li Date: Sun, 17 Oct 2021 21:29:53 +0800 Subject: Fix typos across codebase ./pydis_site/apps/resources/resources/tools/ides/thonny.yaml:1: specifically ./pydis_site/apps/content/resources/guides/pydis-guides/helping-others.md:115: considered ./pydis_site/apps/content/resources/guides/pydis-guides/contributing/issues.md:59 labels ./pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot.md:99: recommend ./pydis_site/apps/content/resources/guides/pydis-guides/contributing/site.md:111: particularly ./pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot/env-var-reference.md:29: Integer ./pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot/env-var-reference.md:67: calculating ./pydis_site/apps/api/tests/test_off_topic_channel_names.py:157: response I didn't touch the code jam and game jam typos because I'm not sure if they should be preserved as is. There were a few 'seperated' typos which I didn't change because I *think* it's just another way of spelling it? In the offensive words test there was a keyword argument named `fied` which I didn't touch because I wasn't sure where that was from. --- pydis_site/apps/api/tests/test_off_topic_channel_names.py | 2 +- .../apps/content/resources/guides/pydis-guides/contributing/issues.md | 2 +- .../resources/guides/pydis-guides/contributing/sir-lancebot.md | 2 +- .../pydis-guides/contributing/sir-lancebot/env-var-reference.md | 4 ++-- .../apps/content/resources/guides/pydis-guides/contributing/site.md | 2 +- .../apps/content/resources/guides/pydis-guides/helping-others.md | 2 +- pydis_site/apps/resources/resources/tools/ides/thonny.yaml | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) 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 63993978..1825f6e6 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 @@ -154,7 +154,7 @@ class DeletionTests(AuthenticatedAPITestCase): cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk') def test_deleting_unknown_name_returns_404(self): - """Return 404 reponse when trying to delete unknown name.""" + """Return 404 response when trying to delete unknown name.""" url = reverse('api:bot:offtopicchannelname-detail', args=('unknown-name',)) response = self.client.delete(url) diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/issues.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/issues.md index 9151e5e3..0c6d3513 100644 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/issues.md +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/issues.md @@ -56,7 +56,7 @@ Definitely try to: Labels allow us to better organise Issues by letting us view what type of Issue it is, how it might impact the codebase and at what stage it's at. -In our repositories, we try to prefix labels belonging to the same group, for example the label groups `status` or `type`. We will be trying to keep to the same general structure across our project repositories, but just have a look at the full lables list in the respective repository to get a clear idea what's available. +In our repositories, we try to prefix labels belonging to the same group, for example the label groups `status` or `type`. We will be trying to keep to the same general structure across our project repositories, but just have a look at the full labels list in the respective repository to get a clear idea what's available. If you're a contributor, you can add relevant labels yourself to any new Issue ticket you create. 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 60169c01..a0d3d463 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 @@ -96,7 +96,7 @@ Otherwise, please see the below linked guide for Redis related variables. --- # Run the project -The sections below describe the two ways you can run this project. We recomend Docker as it requires less setup. +The sections below describe the two ways you can run this project. We recommend Docker as it requires less setup. ## Run with Docker Make sure to have Docker running, then use the Docker command `docker-compose up` in the project root. diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot/env-var-reference.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot/env-var-reference.md index eba737ad..9ad014a2 100644 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot/env-var-reference.md +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot/env-var-reference.md @@ -26,7 +26,7 @@ Additionally, you may find the following environment variables useful during dev | `BOT_DEBUG` | Debug mode of the bot | False | | `PREFIX` | The bot's invocation prefix | `.` | | `CYCLE_FREQUENCY` | Amount of days between cycling server icon | 3 | -| `MONTH_OVERRIDE` | Interger in range `[0, 12]`, overrides current month w.r.t. seasonal decorators | +| `MONTH_OVERRIDE` | Integer in range `[0, 12]`, overrides current month w.r.t. seasonal decorators | | `REDIS_HOST` | The address to connect to for the Redis database. | | `REDIS_PORT` | | | `REDIS_PASSWORD` | | @@ -64,7 +64,7 @@ These variables might come in handy while working on certain cogs: | Advent of Code | `AOC_LEADERBOARDS` | List of leaderboards seperated by `::`. Each entry should have an `id,session cookie,join code` seperated by commas in that order. | | Advent of Code | `AOC_STAFF_LEADERBOARD_ID` | Integer ID of the staff leaderboard. | | Advent of Code | `AOC_ROLE_ID` | ID of the advent of code role. -| Advent of Code | `AOC_IGNORED_DAYS` | Comma seperated list of days to ignore while calulating score. | +| Advent of Code | `AOC_IGNORED_DAYS` | Comma seperated list of days to ignore while calculating score. | | Advent of Code | `AOC_YEAR` | Debug variable to change the year used for AoC. | | Advent of Code | `AOC_CHANNEL_ID` | The ID of the #advent-of-code channel | | Advent of Code | `AOC_COMMANDS_CHANNEL_ID` | The ID of the #advent-of-code-commands channel | diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/site.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/site.md index df75e81a..f2c3bd95 100644 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/site.md +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/site.md @@ -108,7 +108,7 @@ If you get any Docker related errors, reference the [Possible Issues](https://py ## Run on the host -Running on the host is particularily useful if you wish to debug the site. The [environment variables](#2-environment-variables) shown in a previous section need to have been configured. +Running on the host is particularly useful if you wish to debug the site. The [environment variables](#2-environment-variables) shown in a previous section need to have been configured. ### Database diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/helping-others.md b/pydis_site/apps/content/resources/guides/pydis-guides/helping-others.md index d126707d..a7f1ce1d 100644 --- a/pydis_site/apps/content/resources/guides/pydis-guides/helping-others.md +++ b/pydis_site/apps/content/resources/guides/pydis-guides/helping-others.md @@ -112,7 +112,7 @@ Presenting a solution that is considered a bad practice might be useful in certa > for i in range(len(your_list)): > print(your_list[i]) > -> The second replier gave a valid solution, but it's important that he clarifies that it is concidered a bad practice in Python, and that the first solution should usually be used in this case. +> The second replier gave a valid solution, but it's important that he clarifies that it is considered a bad practice in Python, and that the first solution should usually be used in this case. ## It's OK to Step Away diff --git a/pydis_site/apps/resources/resources/tools/ides/thonny.yaml b/pydis_site/apps/resources/resources/tools/ides/thonny.yaml index 3581e1cd..d7f03a74 100644 --- a/pydis_site/apps/resources/resources/tools/ides/thonny.yaml +++ b/pydis_site/apps/resources/resources/tools/ides/thonny.yaml @@ -1,4 +1,4 @@ -description: A Python IDE specifially aimed at learning programming. Has a lot of +description: A Python IDE specifically aimed at learning programming. Has a lot of helpful features to help you understand your code. name: Thonny title_url: https://thonny.org/ -- cgit v1.2.3 From b3a5c661e179fbddb2081346aff7a77169df7676 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Sun, 17 Oct 2021 18:46:53 +0400 Subject: Fixes Test Paths For Windows (#596) Fixes a test that fails on Windows due to differing path separators. This is currently the only failing test. Signed-off-by: Hassan Abouelela --- pydis_site/apps/content/tests/test_views.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pydis_site/apps/content/tests/test_views.py b/pydis_site/apps/content/tests/test_views.py index b6e752d6..eadad7e3 100644 --- a/pydis_site/apps/content/tests/test_views.py +++ b/pydis_site/apps/content/tests/test_views.py @@ -167,11 +167,16 @@ class PageOrCategoryViewTests(MockPagesTestCase, SimpleTestCase, TestCase): self.ViewClass.dispatch(request, location="category/subcategory/with_metadata") context = self.ViewClass.get_context_data() + + # Convert to paths to avoid dealing with non-standard path separators + for item in context["breadcrumb_items"]: + item["path"] = Path(item["path"]) + self.assertEquals( context["breadcrumb_items"], [ - {"name": PARSED_CATEGORY_INFO["title"], "path": "."}, - {"name": PARSED_CATEGORY_INFO["title"], "path": "category"}, - {"name": PARSED_CATEGORY_INFO["title"], "path": "category/subcategory"}, + {"name": PARSED_CATEGORY_INFO["title"], "path": Path(".")}, + {"name": PARSED_CATEGORY_INFO["title"], "path": Path("category")}, + {"name": PARSED_CATEGORY_INFO["title"], "path": Path("category/subcategory")}, ] ) -- cgit v1.2.3 From 5972560e59f98fced0c56d70039b0d0ba15532d0 Mon Sep 17 00:00:00 2001 From: Hedy Li Date: Mon, 18 Oct 2021 08:57:54 +0800 Subject: Fix typo 'fied' in apps/api/tests/test_offensive_message.py --- pydis_site/apps/api/tests/test_offensive_message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/apps/api/tests/test_offensive_message.py b/pydis_site/apps/api/tests/test_offensive_message.py index 9b79b38c..3cf95b75 100644 --- a/pydis_site/apps/api/tests/test_offensive_message.py +++ b/pydis_site/apps/api/tests/test_offensive_message.py @@ -58,7 +58,7 @@ class CreationTests(AuthenticatedAPITestCase): ) for field, invalid_value in cases: - with self.subTest(fied=field, invalid_value=invalid_value): + with self.subTest(field=field, invalid_value=invalid_value): test_data = data.copy() test_data.update({field: invalid_value}) -- cgit v1.2.3 From 01db2676c43994755ab18b7609943dae8982c822 Mon Sep 17 00:00:00 2001 From: Hedy Li Date: Mon, 18 Oct 2021 09:26:29 +0800 Subject: Fix typos in contributing guides - succint -> succinct - seperated -> separated --- .../pydis-guides/contributing/sir-lancebot/env-var-reference.md | 4 ++-- .../guides/pydis-guides/contributing/working-with-git/cli.md | 2 +- .../guides/pydis-guides/contributing/working-with-git/pycharm.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot/env-var-reference.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot/env-var-reference.md index 9ad014a2..51587aac 100644 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot/env-var-reference.md +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot/env-var-reference.md @@ -61,10 +61,10 @@ These variables might come in handy while working on certain cogs: | Cog | Environment Variable | Description | | -------- | -------- | -------- | -| Advent of Code | `AOC_LEADERBOARDS` | List of leaderboards seperated by `::`. Each entry should have an `id,session cookie,join code` seperated by commas in that order. | +| Advent of Code | `AOC_LEADERBOARDS` | List of leaderboards separated by `::`. Each entry should have an `id,session cookie,join code` separated by commas in that order. | | Advent of Code | `AOC_STAFF_LEADERBOARD_ID` | Integer ID of the staff leaderboard. | | Advent of Code | `AOC_ROLE_ID` | ID of the advent of code role. -| Advent of Code | `AOC_IGNORED_DAYS` | Comma seperated list of days to ignore while calculating score. | +| Advent of Code | `AOC_IGNORED_DAYS` | Comma separated list of days to ignore while calculating score. | | Advent of Code | `AOC_YEAR` | Debug variable to change the year used for AoC. | | Advent of Code | `AOC_CHANNEL_ID` | The ID of the #advent-of-code channel | | Advent of Code | `AOC_COMMANDS_CHANNEL_ID` | The ID of the #advent-of-code-commands channel | diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/cli.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/cli.md index 5f196837..94f94d57 100644 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/cli.md +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/cli.md @@ -27,7 +27,7 @@ If you use SSH, use `git@github.com:python-discord/sir-lancebot.git` for the ups You will be committing your changes to a new branch rather than to `main`. Using branches allows you to work on muiltiple pull requests without conflicts. -You can name your branch whatever you want, but it's recommended to name it something succint and relevant to the changes you will be making. +You can name your branch whatever you want, but it's recommended to name it something succinct and relevant to the changes you will be making. Run the following commands to create a new branch. Replace `branch_name` with the name you wish to give your branch. ```sh diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/pycharm.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/pycharm.md index 3f7fefa0..e0b2e33c 100644 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/pycharm.md +++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/pycharm.md @@ -27,7 +27,7 @@ The following will use the [Sir-Lancebot](https://github.com/python-discord/sir- ## Creating a New Branch > You will be committing your changes to a new branch rather than to `main`. Using branches allows you to work on multiple pull requests at the same time without conflicts. -> You can name your branch whatever you want, but it's recommended to name it something succint and relevant to the changes you will be making. +> You can name your branch whatever you want, but it's recommended to name it something succinct and relevant to the changes you will be making. > Before making new branches, be sure to checkout the `main` branch and ensure it's up to date. -- cgit v1.2.3 From 99de765b0fb5af4f5313cd23682d94466e1f31fd Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Sat, 23 Oct 2021 15:29:02 +0300 Subject: Correct redis config instructions in bot guide --- .../apps/content/resources/guides/pydis-guides/contributing/bot.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 183f1505..b9589def 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 @@ -607,7 +607,7 @@ In [Configure the Bot](#configyml) you were asked to set `bot.redis.use_fakeredi If you are working on a feature that relies on Redis, you will need to enable Redis to make sure persistency is achieved for the feature across restarts. The first step towards that is going to `config.yml` and setting `bot.redis.use_fakeredis` to `false`. #### Starting Redis in Docker (Recommended) -If you're using the Docker image provided in the project's Docker Compose, open your `config.yml` file, set `bot.redis.host` to `redis`, and `bot.redis.password` to `null`. +If you're using the Docker image provided in the project's Docker Compose, open your `config.yml` file. If you're running the bot in Docker, set `bot.redis.host` to `redis`, and if you're running it on the host set it to `localhost`. Set `bot.redis.password` to `null`. #### Starting Redis Using Other Methods You can run your own instance of Redis, but in that case you will need to correctly set `bot.redis.host` and `bot.redis.port`, and the `bot.redis.password` value in `config-default.yml` should not be overridden. Then, enter the `.env` file, and set `REDIS_PASSWORD` to whatever password you set. -- cgit v1.2.3 From 367c01a7b3cd5cc5ee25644ae92f917ceb00562c Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Fri, 29 Oct 2021 03:19:37 +0400 Subject: Adds Failures Field To Reminders Adds an integer field to reminders, to keep track of how many failed attempts at delivering the reminder were made. Updates documentation and serialization. Signed-off-by: Hassan Abouelela --- .../apps/api/migrations/0074_reminder_failures.py | 18 ++++++++++++++++++ pydis_site/apps/api/models/bot/reminder.py | 4 ++++ pydis_site/apps/api/serializers.py | 10 +++++++++- pydis_site/apps/api/viewsets/bot/reminder.py | 13 ++++++++----- 4 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 pydis_site/apps/api/migrations/0074_reminder_failures.py diff --git a/pydis_site/apps/api/migrations/0074_reminder_failures.py b/pydis_site/apps/api/migrations/0074_reminder_failures.py new file mode 100644 index 00000000..2860046e --- /dev/null +++ b/pydis_site/apps/api/migrations/0074_reminder_failures.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.14 on 2021-10-27 17:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0073_otn_allow_GT_and_LT'), + ] + + operations = [ + migrations.AddField( + model_name='reminder', + name='failures', + field=models.IntegerField(default=0, help_text='Number of times we attempted to send the reminder and failed.'), + ), + ] diff --git a/pydis_site/apps/api/models/bot/reminder.py b/pydis_site/apps/api/models/bot/reminder.py index 7d968a0e..173900ee 100644 --- a/pydis_site/apps/api/models/bot/reminder.py +++ b/pydis_site/apps/api/models/bot/reminder.py @@ -59,6 +59,10 @@ class Reminder(ModelReprMixin, models.Model): blank=True, help_text="IDs of roles or users to ping with the reminder." ) + failures = models.IntegerField( + default=0, + help_text="Number of times we attempted to send the reminder and failed." + ) def __str__(self): """Returns some info on the current reminder, for display purposes.""" diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index f47bedca..3e213d43 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -231,7 +231,15 @@ class ReminderSerializer(ModelSerializer): model = Reminder fields = ( - 'active', 'author', 'jump_url', 'channel_id', 'content', 'expiration', 'id', 'mentions' + 'active', + 'author', + 'jump_url', + 'channel_id', + 'content', + 'expiration', + 'id', + 'mentions', + 'failures' ) diff --git a/pydis_site/apps/api/viewsets/bot/reminder.py b/pydis_site/apps/api/viewsets/bot/reminder.py index 111660d9..78d7cb3b 100644 --- a/pydis_site/apps/api/viewsets/bot/reminder.py +++ b/pydis_site/apps/api/viewsets/bot/reminder.py @@ -42,7 +42,8 @@ class ReminderViewSet( ... 'expiration': '5018-11-20T15:52:00Z', ... 'id': 11, ... 'channel_id': 634547009956872193, - ... 'jump_url': "https://discord.com/channels///" + ... 'jump_url': "https://discord.com/channels///", + ... 'failures': 3 ... }, ... ... ... ] @@ -67,7 +68,8 @@ class ReminderViewSet( ... 'expiration': '5018-11-20T15:52:00Z', ... 'id': 11, ... 'channel_id': 634547009956872193, - ... 'jump_url': "https://discord.com/channels///" + ... 'jump_url': "https://discord.com/channels///", + ... 'failures': 3 ... } #### Status codes @@ -80,7 +82,7 @@ class ReminderViewSet( #### Request body >>> { ... 'author': int, - ... 'mentions': List[int], + ... 'mentions': list[int], ... 'content': str, ... 'expiration': str, # ISO-formatted datetime ... 'channel_id': int, @@ -98,9 +100,10 @@ class ReminderViewSet( #### Request body >>> { - ... 'mentions': List[int], + ... 'mentions': list[int], ... 'content': str, - ... 'expiration': str # ISO-formatted datetime + ... 'expiration': str, # ISO-formatted datetime + ... 'failures': int ... } #### Status codes -- cgit v1.2.3 From e981db620f9f7ee05d4f42ad8b4ebb4fcff57f14 Mon Sep 17 00:00:00 2001 From: Hedy Li Date: Thu, 4 Nov 2021 13:04:46 +0800 Subject: Improve Gitpod section in Sir Lancebot contributing guide - Add newlines so it's not a big unfriendly paragraph - Fix gitpod workspace link - Clarification on terminal commands - Clarification on test server and bot - Add links for that ^ and for environment variables --- .../resources/guides/pydis-guides/contributing/sir-lancebot.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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..53674063 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/#/python-discord/sir-lancebot](https://gitpod.io/#/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. -- cgit v1.2.3 From 8faaddceb37cd71d9c11ec827a71b3b67521fbe0 Mon Sep 17 00:00:00 2001 From: Hedy Li Date: Thu, 4 Nov 2021 13:18:38 +0800 Subject: Fix gitpod link in Sir Lancebot contributing guide --- .../content/resources/guides/pydis-guides/contributing/sir-lancebot.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 53674063..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 @@ -21,7 +21,7 @@ Sir Lancebot can be edited and tested on Gitpod. Gitpod will automatically insta 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/#/python-discord/sir-lancebot](https://gitpod.io/#/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: +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 -- cgit v1.2.3 From 2b95db672e649d9fc1159b3e2211dcb72560d8f6 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Fri, 5 Nov 2021 10:49:30 +0530 Subject: Fix: Conflicting migrations. --- pydis_site/apps/api/migrations/0074_merge_20211105_0518.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0074_merge_20211105_0518.py 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 = [ + ] -- cgit v1.2.3 From 876daaf6eb34e620710473c0311e23f36fd4e7eb Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Fri, 5 Nov 2021 10:55:09 +0530 Subject: Update test cases to adhere to recent changes made that removed hosts. --- pydis_site/apps/api/tests/test_off_topic_channel_names.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 354cda9c..2d273756 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 @@ -112,7 +112,7 @@ class ListTests(AuthenticatedAPITestCase): def test_returns_inactive_ot_names(self): """Return inactive off topic names.""" - url = reverse('bot:offtopicchannelname-list') + url = reverse('api:bot:offtopicchannelname-list') response = self.client.get(f"{url}?active=false") self.assertEqual(response.status_code, 200) @@ -123,7 +123,7 @@ class ListTests(AuthenticatedAPITestCase): def test_returns_active_ot_names(self): """Return active off topic names.""" - url = reverse('bot:offtopicchannelname-list') + url = reverse('api:bot:offtopicchannelname-list') response = self.client.get(f"{url}?active=true") self.assertEqual(response.status_code, 200) -- cgit v1.2.3 From 41ec1fd7e3d5bd7b0c16a32b5977d46ce5b3e89e Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Fri, 5 Nov 2021 10:56:01 +0530 Subject: Eliminate usage of typing module and update docstrings. --- pydis_site/apps/api/serializers.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 957c85f3..9b351be2 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -1,6 +1,4 @@ """Converters from Django models to data interchange formats and back.""" -from typing import List - from django.db.models.query import QuerySet from django.db.utils import IntegrityError from rest_framework.exceptions import NotFound @@ -205,12 +203,12 @@ class ExpandedInfractionSerializer(InfractionSerializer): class OffTopicChannelNameListSerializer(ListSerializer): """Custom ListSerializer to override to_representation() when list views are triggered.""" - def to_representation(self, objects: List[OffTopicChannelName]) -> List[str]: + def to_representation(self, objects: list[OffTopicChannelName]) -> list[str]: """ - Return a list representing a list of `OffTopicChannelName`. + Return a list with all `OffTopicChannelName`s in the database. - 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. + 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. """ -- cgit v1.2.3 From 6e1d38d6ceb97f287f0d347e974bc795dda38ffe Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Fri, 5 Nov 2021 10:56:34 +0530 Subject: Use framework defination of method. --- pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 7151d29b..27eabec9 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 @@ -70,7 +70,7 @@ class OffTopicChannelNameViewSet(ModelViewSet): def get_queryset(self, **kwargs) -> QuerySet: """Returns a queryset that covers the entire OffTopicChannelName table.""" - return OffTopicChannelName.objects.filter(**kwargs) + return OffTopicChannelName.objects.all() def create(self, request: Request, *args, **kwargs) -> Response: """ @@ -137,6 +137,6 @@ class OffTopicChannelNameViewSet(ModelViewSet): if active_param := request.query_params.get("active"): params["active"] = active_param.lower() == "true" - queryset = self.get_queryset(**params) + queryset = self.get_queryset().filter(**params) serialized = self.serializer_class(queryset, many=True) return Response(serialized.data) -- cgit v1.2.3 From 4377c6d960e3fbf3a416230d1ad7de972019898e Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Sat, 6 Nov 2021 14:43:17 +0530 Subject: Remove get_queryset() and add new class variable . --- pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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 27eabec9..78f8c340 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 @@ -57,6 +57,7 @@ class OffTopicChannelNameViewSet(ModelViewSet): lookup_field = 'name' serializer_class = OffTopicChannelNameSerializer + queryset = OffTopicChannelName.objects.all() def get_object(self) -> OffTopicChannelName: """ @@ -64,11 +65,10 @@ class OffTopicChannelNameViewSet(ModelViewSet): 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, **kwargs) -> QuerySet: + def get_queryset(self) -> QuerySet: """Returns a queryset that covers the entire OffTopicChannelName table.""" return OffTopicChannelName.objects.all() @@ -108,13 +108,13 @@ class OffTopicChannelNameViewSet(ModelViewSet): 'random_items': ["Must be a positive integer."] }) - queryset = self.get_queryset().order_by('used', '?')[:random_count] + queryset = self.queryset.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), @@ -125,7 +125,7 @@ class OffTopicChannelNameViewSet(ModelViewSet): ) 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) @@ -137,6 +137,6 @@ class OffTopicChannelNameViewSet(ModelViewSet): if active_param := request.query_params.get("active"): params["active"] = active_param.lower() == "true" - queryset = self.get_queryset().filter(**params) + queryset = self.queryset.filter(**params) serialized = self.serializer_class(queryset, many=True) return Response(serialized.data) -- cgit v1.2.3 From 7353e1ed7003a1198fb00a1e40251b95cf2fdf7e Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Wed, 17 Nov 2021 14:27:45 +0400 Subject: Adds Redirect Filter List Adds a new filter list for URLs which should be treated as redirects and unfurled. Signed-off-by: Hassan Abouelela --- .../apps/api/migrations/0075_add_redirects_filter.py | 18 ++++++++++++++++++ pydis_site/apps/api/models/bot/filter_list.py | 1 + pydis_site/apps/api/viewsets/bot/filter_list.py | 3 ++- 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 pydis_site/apps/api/migrations/0075_add_redirects_filter.py 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/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/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 -- cgit v1.2.3 From 855bf5e7c7e54e9f8e4544545026c47092b52a11 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 22 Nov 2021 20:36:21 +0000 Subject: Only calc activity blocks when <1k messages We only truly care about how many activity blocks a user has when they have a small number of messages, as we only use this for the voice gate. If a user has more than say 1k messages, then we really don't need to calculate their activity blocks, as it's quite an expensive query with many messages. --- pydis_site/apps/api/viewsets/bot/user.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 22d13dc4..ed661323 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -271,9 +271,15 @@ class UserViewSet(ModelViewSet): with Metricity() as metricity: try: data = metricity.user(user.id) + data["total_messages"] = metricity.total_messages(user.id) + if data["total_messages"] < 1000: + # Only calculate and return activity_blocks if the user has a small amount + # of messages, as calculating activity_blocks is expensive. + # 1000 message chosen as an arbitrarily large number. + data["activity_blocks"] = metricity.total_message_blocks(user.id) + data["voice_banned"] = voice_banned - data["activity_blocks"] = metricity.total_message_blocks(user.id) return Response(data, status=status.HTTP_200_OK) except NotFoundError: return Response(dict(detail="User not found in metricity"), -- cgit v1.2.3 From c8d1513b8f8cb21482180ce19d69a107adefe4e2 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 22 Nov 2021 20:38:41 +0000 Subject: Make metricity test order insensitive We only actually care that thee key:value pairs are equal, the order of them isn't actually important. The naming of `assertCountEqual` is a little misleading, since it actually tests that the first sequence contains the same elements as second, regardless of their order. See https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertCountEqual --- pydis_site/apps/api/tests/test_users.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py index 295bcf64..550c7240 100644 --- a/pydis_site/apps/api/tests/test_users.py +++ b/pydis_site/apps/api/tests/test_users.py @@ -408,7 +408,7 @@ class UserMetricityTests(AuthenticatedAPITestCase): in_guild=True, ) - def test_get_metricity_data(self): + def test_get_metricity_data_under_1k(self): # Given joined_at = "foo" total_messages = 1 @@ -421,7 +421,7 @@ 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, -- cgit v1.2.3 From 78b2f3b8ed46d23015ab2f765504572f672f4567 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 22 Nov 2021 20:38:58 +0000 Subject: Add metricity test for users >1k messages --- pydis_site/apps/api/tests/test_users.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py index 550c7240..81bfd43b 100644 --- a/pydis_site/apps/api/tests/test_users.py +++ b/pydis_site/apps/api/tests/test_users.py @@ -428,6 +428,25 @@ class UserMetricityTests(AuthenticatedAPITestCase): "activity_blocks": total_blocks }) + def test_get_metricity_data_over_1k(self): + # Given + joined_at = "foo" + total_messages = 1001 + total_blocks = 1001 + self.mock_metricity_user(joined_at, total_messages, total_blocks, []) + + # When + url = reverse('api:bot:user-metricity-data', args=[0]) + response = self.client.get(url) + + # Then + self.assertEqual(response.status_code, 200) + self.assertCountEqual(response.json(), { + "joined_at": joined_at, + "total_messages": total_messages, + "voice_banned": False, + }) + def test_no_metricity_user(self): # Given self.mock_no_metricity_user() -- cgit v1.2.3 From 5b1bb82165d188a571c8999f17ef52f3856c9518 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 23 Nov 2021 16:29:06 +0000 Subject: Alter message query to leverage index Previously this query would convert each row to an array just to check if it matched or not. By changing EXCLUDE_CHANNELS to a tuple instead of a list, it doesn't get passed as an array, so we can do a simple NOT IN check. This will also allow us to add an index with this condition to speed it up further. --- pydis_site/apps/api/models/bot/metricity.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py index 33fb7ad7..901f191a 100644 --- a/pydis_site/apps/api/models/bot/metricity.py +++ b/pydis_site/apps/api/models/bot/metricity.py @@ -4,10 +4,10 @@ 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): @@ -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; """, -- cgit v1.2.3 From 61f9daed8d71046351e60c57ccdbf34443085561 Mon Sep 17 00:00:00 2001 From: Izan Date: Wed, 10 Nov 2021 22:44:20 +0000 Subject: Add `dm_sent` field to infractions model & serializer --- .../apps/api/migrations/0075_infraction_dm_sent.py | 18 ++++++++++++++++++ pydis_site/apps/api/models/bot/infraction.py | 4 ++++ pydis_site/apps/api/serializers.py | 2 +- 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 pydis_site/apps/api/migrations/0075_infraction_dm_sent.py 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/models/bot/infraction.py b/pydis_site/apps/api/models/bot/infraction.py index 60c1e8dd..913631d4 100644 --- a/pydis_site/apps/api/models/bot/infraction.py +++ b/pydis_site/apps/api/models/bot/infraction.py @@ -57,6 +57,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/serializers.py b/pydis_site/apps/api/serializers.py index 3e213d43..f6801597 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -145,7 +145,7 @@ 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( -- cgit v1.2.3 From 1e3725d8f6ef1a601c12054db3d913e447b51866 Mon Sep 17 00:00:00 2001 From: Izan Date: Fri, 12 Nov 2021 14:31:52 +0000 Subject: Update documentation to include `dm_sent` field --- pydis_site/apps/api/viewsets/bot/infraction.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 -- cgit v1.2.3 From 546a2d18bb52a0b4f1fc31de765119623a97f7fa Mon Sep 17 00:00:00 2001 From: Izan Date: Thu, 25 Nov 2021 10:44:06 +0000 Subject: Fix linting --- pydis_site/apps/api/serializers.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index f6801597..de2fccff 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', 'dm_sent' + 'id', + 'inserted_at', + 'expires_at', + 'active', + 'user', + 'actor', + 'type', + 'reason', + 'hidden', + 'dm_sent' ) validators = [ UniqueTogetherValidator( -- cgit v1.2.3 From 454ad492229253ae675f4ba956c74edf32f41c2a Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Thu, 25 Nov 2021 20:41:16 +0100 Subject: Merge migrations with upstream --- pydis_site/apps/api/migrations/0076_merge_20211125_1941.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0076_merge_20211125_1941.py 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 = [ + ] -- cgit v1.2.3 From f093907393b4f479f62409286831b938ba65c558 Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Tue, 31 Aug 2021 16:27:18 +0200 Subject: Create a signal to unassign roles from user when deleted Add a signal to the api app that automatically unassigns deleted roles from users that have them --- pydis_site/apps/api/__init__.py | 1 + pydis_site/apps/api/apps.py | 10 +++++++++- pydis_site/apps/api/signals.py | 12 ++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 pydis_site/apps/api/signals.py 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/signals.py b/pydis_site/apps/api/signals.py new file mode 100644 index 00000000..c69704b1 --- /dev/null +++ b/pydis_site/apps/api/signals.py @@ -0,0 +1,12 @@ +from django.db.models.signals import pre_delete +from django.dispatch import receiver + +from pydis_site.apps.api.models.bot import Role, User + + +@receiver(signal=pre_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() -- cgit v1.2.3 From 0f24bdcd0ba261da045ac73e8567239eb63c6fc6 Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Tue, 31 Aug 2021 17:54:39 +0200 Subject: Add test to check role unassignment Create a test that checks if a role gets deleted it will also get unassigned from the user --- pydis_site/apps/api/tests/test_roles.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/pydis_site/apps/api/tests/test_roles.py b/pydis_site/apps/api/tests/test_roles.py index d39cea4d..7c968852 100644 --- a/pydis_site/apps/api/tests/test_roles.py +++ b/pydis_site/apps/api/tests/test_roles.py @@ -1,7 +1,8 @@ from django.urls import reverse from .base import AuthenticatedAPITestCase -from ..models import Role +from ..models import Role, User + class CreationTests(AuthenticatedAPITestCase): @@ -35,6 +36,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.""" @@ -181,6 +196,11 @@ 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.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,)) -- cgit v1.2.3 From 9d255dcf3daafde71071ad75b000077a861da659 Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Tue, 31 Aug 2021 18:49:21 +0200 Subject: Patch roles test to use fresh instance from the DB --- pydis_site/apps/api/tests/test_roles.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pydis_site/apps/api/tests/test_roles.py b/pydis_site/apps/api/tests/test_roles.py index 7c968852..88c0256b 100644 --- a/pydis_site/apps/api/tests/test_roles.py +++ b/pydis_site/apps/api/tests/test_roles.py @@ -199,6 +199,7 @@ class CreationTests(AuthenticatedAPITestCase): 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): -- cgit v1.2.3 From f34a52016bd5a7d50c1146d6fbd213ce889f58c7 Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Tue, 31 Aug 2021 19:21:36 +0200 Subject: Patch signals to use post_delete, instead of pre_delete From now on the signal will only get executed after the Role has been deleted The commit also introduces minor changes in the tests of roles --- pydis_site/apps/api/signals.py | 4 ++-- pydis_site/apps/api/tests/test_roles.py | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pydis_site/apps/api/signals.py b/pydis_site/apps/api/signals.py index c69704b1..5c26bfb6 100644 --- a/pydis_site/apps/api/signals.py +++ b/pydis_site/apps/api/signals.py @@ -1,10 +1,10 @@ -from django.db.models.signals import pre_delete +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=pre_delete, sender=Role) +@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]): diff --git a/pydis_site/apps/api/tests/test_roles.py b/pydis_site/apps/api/tests/test_roles.py index 88c0256b..73c80c77 100644 --- a/pydis_site/apps/api/tests/test_roles.py +++ b/pydis_site/apps/api/tests/test_roles.py @@ -4,7 +4,6 @@ from .base import AuthenticatedAPITestCase from ..models import Role, User - class CreationTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): @@ -96,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) -- cgit v1.2.3 From 65d8205dc3dbfc95f187836dc8da6ef08352dd52 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Fri, 26 Nov 2021 23:18:54 +0300 Subject: Fix Typo In Static Task --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2f1322e3..2de2bf35 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,4 +54,4 @@ test = "coverage run manage.py test" 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" -- cgit v1.2.3 From 77da014e5c3e6c7eebf5d4d23d8aca9ca4a500f7 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Fri, 26 Nov 2021 23:24:08 +0300 Subject: Clean Up Static Deploys Readme Reorganize the static deploy readme, and clarify the content. --- static-builds/README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) 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. -- cgit v1.2.3 From 0150b4b6b99a37052a5a1bbe0d1ff82bd4979521 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Nov 2021 20:33:15 +0000 Subject: Bump django from 3.0.14 to 3.1.13 Bumps [django](https://github.com/django/django) from 3.0.14 to 3.1.13. - [Release notes](https://github.com/django/django/releases) - [Commits](https://github.com/django/django/compare/3.0.14...3.1.13) --- updated-dependencies: - dependency-name: django dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- poetry.lock | 11 ++++++----- pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index eac58fdb..bd2e506c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -122,14 +122,14 @@ python-versions = "*" [[package]] name = "django" -version = "3.0.14" +version = "3.1.13" 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" @@ -769,7 +769,7 @@ brotli = ["brotli"] [metadata] lock-version = "1.1" python-versions = "3.9.*" -content-hash = "9f0c069c14e2dbff63d58474702693f0c02b8cfd30e5af38303975a73b71bcfd" +content-hash = "4588ff7b75b20c57c5ab674486764cda32de90af35f3e59a5ca8706aabeeb566" [metadata.files] asgiref = [ @@ -867,8 +867,8 @@ distlib = [ {file = "distlib-0.3.3.zip", hash = "sha256:d982d0751ff6eaaab5e2ec8e691d949ee80eddf01a62eaa96ddb11531fe16b05"}, ] 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.13-py3-none-any.whl", hash = "sha256:a6e0d1ff11095b7394c079ade7094c73b2dc3df4a7a373c9b58ed73b77a97feb"}, + {file = "Django-3.1.13.tar.gz", hash = "sha256:9f8be75646f62204320b195062b1d696ba28aa3d45ee72fb7c888ffaebc5bdb2"}, ] django-distill = [ {file = "django-distill-2.9.0.tar.gz", hash = "sha256:08f31dcde2e79e73c0bc4f36941830603a811cc89472be11f79f14affb460d84"}, @@ -967,6 +967,7 @@ 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 = [ diff --git a/pyproject.toml b/pyproject.toml index 2de2bf35..10b2d75b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ license = "MIT" [tool.poetry.dependencies] python = "3.9.*" -django = "~=3.0.4" +django = "~=3.1.13" django-environ = "~=0.4.5" django-filter = "~=2.1.0" djangorestframework = "~=3.11.0" -- cgit v1.2.3 From 3f4adf3768123c870f9da38a23b189862a7b9502 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sat, 27 Nov 2021 13:28:23 +0100 Subject: Migrate to generic `JSONField` --- .../api/migrations/0077_use_generic_jsonfield.py | 25 ++++++++++++++++++++++ pydis_site/apps/api/models/bot/bot_setting.py | 3 +-- pydis_site/apps/api/models/bot/message.py | 2 +- pydis_site/apps/api/models/utils.py | 3 +-- 4 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 pydis_site/apps/api/migrations/0077_use_generic_jsonfield.py 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/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/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/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, ... ) -- cgit v1.2.3 From 73bbd7908a084484cba283c0243d67bd3339b953 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sat, 27 Nov 2021 13:29:01 +0100 Subject: Update `django-filter` for new Django version --- poetry.lock | 12 ++++++------ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index bd2e506c..23dcfbb1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -159,14 +159,14 @@ 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" @@ -769,7 +769,7 @@ brotli = ["brotli"] [metadata] lock-version = "1.1" python-versions = "3.9.*" -content-hash = "4588ff7b75b20c57c5ab674486764cda32de90af35f3e59a5ca8706aabeeb566" +content-hash = "52569d311f19fb121cd6f824bf35bdb9611705b393d8aa013b8a1ce6e4ebbcbb" [metadata.files] asgiref = [ @@ -878,8 +878,8 @@ django-environ = [ {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"}, diff --git a/pyproject.toml b/pyproject.toml index 10b2d75b..14310ce8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ license = "MIT" python = "3.9.*" django = "~=3.1.13" django-environ = "~=0.4.5" -django-filter = "~=2.1.0" +django-filter = "~=21.1" djangorestframework = "~=3.11.0" psycopg2-binary = "~=2.8.0" django-simple-bulma = "~=2.1" -- cgit v1.2.3 From 2b5642cf8cc8a97eb5c91b06d9fda57e9b67597a Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Fri, 10 Dec 2021 13:46:14 +0100 Subject: Remove myself from the codeowners --- .github/CODEOWNERS | 13 ++++--------- 1 file changed, 4 insertions(+), 9 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 -- cgit v1.2.3 From 953f232d47ff4e07baeaffa8add7912ff05d6457 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 11 Dec 2021 13:59:40 +0000 Subject: Use new approx message count view We have added a new view to metricity that will keep track of an approximate message count, updating every 10 seconds. By doing this, we avoid running a query against the whole message table every time we want to get a user's messages. --- pydis_site/apps/api/models/bot/metricity.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py index 901f191a..52e946ac 100644 --- a/pydis_site/apps/api/models/bot/metricity.py +++ b/pydis_site/apps/api/models/bot/metricity.py @@ -46,14 +46,12 @@ class Metricity: self.cursor.execute( """ SELECT - COUNT(*) - FROM messages + message_count + FROM user_has_approx_message_count WHERE author_id = '%s' - AND NOT is_deleted - AND channel_id NOT IN %s """, - [user_id, EXCLUDE_CHANNELS] + [user_id] ) values = self.cursor.fetchone() -- cgit v1.2.3 From bd9dadedd80c7158fd43889876d9978ea7b2ea5d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 11 Dec 2021 14:38:29 +0000 Subject: Bump django from 3.1.13 to 3.1.14 Bumps [django](https://github.com/django/django) from 3.1.13 to 3.1.14. - [Release notes](https://github.com/django/django/releases) - [Commits](https://github.com/django/django/compare/3.1.13...3.1.14) --- updated-dependencies: - dependency-name: django dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 23dcfbb1..eb10aecc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -122,7 +122,7 @@ python-versions = "*" [[package]] name = "django" -version = "3.1.13" +version = "3.1.14" description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." category = "main" optional = false @@ -769,7 +769,7 @@ brotli = ["brotli"] [metadata] lock-version = "1.1" python-versions = "3.9.*" -content-hash = "52569d311f19fb121cd6f824bf35bdb9611705b393d8aa013b8a1ce6e4ebbcbb" +content-hash = "fa1fc3596f32e88a15696551d172b9d1da97acebbba224915c48fb6692996e56" [metadata.files] asgiref = [ @@ -867,8 +867,8 @@ distlib = [ {file = "distlib-0.3.3.zip", hash = "sha256:d982d0751ff6eaaab5e2ec8e691d949ee80eddf01a62eaa96ddb11531fe16b05"}, ] django = [ - {file = "Django-3.1.13-py3-none-any.whl", hash = "sha256:a6e0d1ff11095b7394c079ade7094c73b2dc3df4a7a373c9b58ed73b77a97feb"}, - {file = "Django-3.1.13.tar.gz", hash = "sha256:9f8be75646f62204320b195062b1d696ba28aa3d45ee72fb7c888ffaebc5bdb2"}, + {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"}, diff --git a/pyproject.toml b/pyproject.toml index 14310ce8..8da2f51e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ license = "MIT" [tool.poetry.dependencies] python = "3.9.*" -django = "~=3.1.13" +django = "~=3.1.14" django-environ = "~=0.4.5" django-filter = "~=21.1" djangorestframework = "~=3.11.0" -- cgit v1.2.3 From 1261efd75c10bf2031a2019475d4b32c5859d807 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Mon, 13 Dec 2021 11:25:27 +0530 Subject: Merge migrations to avoid conflicts. --- pydis_site/apps/api/migrations/0078_merge_20211213_0552.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0078_merge_20211213_0552.py 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 = [ + ] -- cgit v1.2.3 From 5c4c6328da2a91aecf5cd927e72446b7e0f8ab78 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 13 Dec 2021 19:09:03 +0000 Subject: Bump drf up to 3.12.0 This is to add support for Django 3.1's database-agnositic JSONField. --- poetry.lock | 199 ++++++++++++++++++++++++++++++++++----------------------- pyproject.toml | 2 +- 2 files changed, 119 insertions(+), 82 deletions(-) diff --git a/poetry.lock b/poetry.lock index eb10aecc..fbf6d8cd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -25,7 +25,7 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (> [[package]] name = "backports.entry-points-selectable" -version = "1.1.0" +version = "1.1.1" description = "Compatibility shim providing selectable entry points for older implementations" category = "dev" optional = false @@ -33,11 +33,11 @@ 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)"] +testing = ["pytest", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"] [[package]] name = "bandit" -version = "1.7.0" +version = "1.7.1" description = "Security oriented static analyser for python code." category = "dev" optional = false @@ -47,12 +47,11 @@ python-versions = ">=3.5" 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]] 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 +67,7 @@ python-versions = ">=3.6.1" [[package]] name = "charset-normalizer" -version = "2.0.6" +version = "2.0.9" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false @@ -114,7 +113,7 @@ yaml = ["PyYAML (>=3.10)"] [[package]] name = "distlib" -version = "0.3.3" +version = "0.3.4" description = "Distribution utilities" category = "dev" optional = false @@ -139,7 +138,7 @@ bcrypt = ["bcrypt"] [[package]] name = "django-distill" -version = "2.9.0" +version = "2.9.1" description = "Static site renderer and publisher for Django." category = "main" optional = false @@ -196,14 +195,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,7 +214,7 @@ python-versions = "*" [[package]] name = "filelock" -version = "3.3.0" +version = "3.4.0" description = "A platform independent file lock." category = "dev" optional = false @@ -240,14 +239,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 +324,14 @@ flake8 = "*" [[package]] name = "flake8-tidy-imports" -version = "4.4.1" +version = "4.5.0" description = "A flake8 plugin that helps you write tidier imports." category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] -flake8 = ">=3.8.0,<4" +flake8 = ">=3.8.0,<5" [[package]] name = "flake8-todo" @@ -347,14 +346,14 @@ 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" @@ -384,23 +383,39 @@ tornado = ["tornado (>=0.2)"] [[package]] name = "identify" -version = "2.3.0" +version = "2.4.0" description = "File identification library for Python" category = "dev" optional = false python-versions = ">=3.6.1" [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.8.2" +description = "Read metadata from Python packages" +category = "main" +optional = false +python-versions = ">=3.6" + +[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", "pep517", "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" @@ -414,12 +429,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 +467,7 @@ python-versions = "*" [[package]] name = "pbr" -version = "5.6.0" +version = "5.8.0" description = "Python Build Reasonableness" category = "dev" optional = false @@ -481,7 +499,7 @@ test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock [[package]] name = "pre-commit" -version = "2.15.0" +version = "2.16.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false @@ -497,7 +515,7 @@ virtualenv = ">=20.0.8" [[package]] name = "prometheus-client" -version = "0.11.0" +version = "0.12.0" description = "Python client for the Prometheus monitoring system." category = "main" optional = false @@ -661,15 +679,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 +703,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 @@ -717,11 +735,11 @@ 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+" +version = "4.0.1" +description = "Backported and Experimental Type Hints for Python 3.6+" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.6" [[package]] name = "urllib3" @@ -738,7 +756,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.8.1" +version = "20.10.0" description = "Virtual Python Environment builder" category = "dev" optional = false @@ -747,12 +765,12 @@ 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,10 +784,22 @@ python-versions = ">=3.5, <4" [package.extras] brotli = ["brotli"] +[[package]] +name = "zipp" +version = "3.6.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.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 = "fa1fc3596f32e88a15696551d172b9d1da97acebbba224915c48fb6692996e56" +content-hash = "2dddd404fff2345135222a309872bfb84c46537ba4f8994e39d6bc563a209522" [metadata.files] asgiref = [ @@ -781,24 +811,24 @@ attrs = [ {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 = "backports.entry_points_selectable-1.1.1-py2.py3-none-any.whl", hash = "sha256:7fceed9532a7aa2bd888654a7314f864a3c16a4e710b34a58cfc0f08114c663b"}, + {file = "backports.entry_points_selectable-1.1.1.tar.gz", hash = "sha256:914b21a479fde881635f7af5adc7f6e38d6b274be32269070c53b698c60d5386"}, ] 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.1-py3-none-any.whl", hash = "sha256:f5acd838e59c038a159b5c621cf0f8270b279e884eadd7b782d7491c02add0d4"}, + {file = "bandit-1.7.1.tar.gz", hash = "sha256:a81b00b5436e6880fa8ad6799bc830e02032047713cbb143a12939ac67eb756c"}, ] 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.9.tar.gz", hash = "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c"}, + {file = "charset_normalizer-2.0.9-py3-none-any.whl", hash = "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, @@ -863,15 +893,15 @@ 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.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.1.tar.gz", hash = "sha256:d849f84f6b763c26980d6bbdecfb1bd69859effef42704fee84f34fa57c82554"}, ] django-environ = [ {file = "django-environ-0.4.5.tar.gz", hash = "sha256:6c9d87660142608f63ec7d5ce5564c49b603ea8ff25da595fd6098f6dc82afde"}, @@ -890,23 +920,23 @@ django-simple-bulma = [ {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.0-py3-none-any.whl", hash = "sha256:2e139a228bcf56dd8b2274a65174d005c4a6b68540ee0bdbb92c76f43f29f7e8"}, + {file = "filelock-3.4.0.tar.gz", hash = "sha256:93d512b32a23baf4cac44ffd72ccf70732aeff7b8050fcaf6d3ec406d954baf4"}, ] 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,15 +962,15 @@ 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.5.0.tar.gz", hash = "sha256:ac637961d0f319012d099e49619f8c928e3221f74e00fe6eb89513bc64c40adb"}, + {file = "flake8_tidy_imports-4.5.0-py3-none-any.whl", hash = "sha256:87eed94ae6a2fda6a5918d109746feadf1311e0eb8274ab7a7920f6db00a41c9"}, ] 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"}, @@ -951,12 +981,16 @@ gunicorn = [ {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.0-py2.py3-none-any.whl", hash = "sha256:eba31ca80258de6bb51453084bff4a923187cd2193b9c13710f2516ab30732cc"}, + {file = "identify-2.4.0.tar.gz", hash = "sha256:a33ae873287e81651c7800ca309dc1f84679b763c9c8b30680e16fbfa82f0107"}, ] 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.8.2-py3-none-any.whl", hash = "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100"}, + {file = "importlib_metadata-4.8.2.tar.gz", hash = "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb"}, ] libsass = [ {file = "libsass-0.21.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:06c8776417fe930714bdc930a3d7e795ae3d72be6ac883ff72a1b8f7c49e5ffb"}, @@ -971,8 +1005,8 @@ libsass = [ {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"}, @@ -987,8 +1021,8 @@ 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"}, @@ -999,12 +1033,12 @@ platformdirs = [ {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, ] 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.16.0-py2.py3-none-any.whl", hash = "sha256:758d1dc9b62c2ed8881585c254976d66eae0889919ab9b859064fc2fe3c7743e"}, + {file = "pre_commit-2.16.0.tar.gz", hash = "sha256:fe9897cac830aa7164dbd02a4e7b90cae49630451ce88464bca73db486ba9f65"}, ] 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.12.0-py2.py3-none-any.whl", hash = "sha256:317453ebabff0a1b02df7f708efbab21e3489e7072b61cb6957230dd004a0af0"}, + {file = "prometheus_client-0.12.0.tar.gz", hash = "sha256:1b12ba48cee33b9b0b9de64a1047cbd3c5f2d0ab6ebcead7ddda613a750ec3c5"}, ] psutil = [ {file = "psutil-5.8.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0066a82f7b1b37d334e68697faba68e5ad5e858279fd6351c8ca6024e8d6ba64"}, @@ -1145,20 +1179,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"}, @@ -1169,19 +1203,22 @@ toml = [ {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"}, + {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, + {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, ] urllib3 = [ {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, ] 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.10.0-py2.py3-none-any.whl", hash = "sha256:4b02e52a624336eece99c96e3ab7111f469c24ba226a53ec474e8e787b365814"}, + {file = "virtualenv-20.10.0.tar.gz", hash = "sha256:576d05b46eace16a9c348085f7d0dc8ef28713a2cabaa1cf0aea41e8f12c9218"}, ] 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.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, + {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, +] diff --git a/pyproject.toml b/pyproject.toml index 8da2f51e..258772c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ python = "3.9.*" django = "~=3.1.14" django-environ = "~=0.4.5" django-filter = "~=21.1" -djangorestframework = "~=3.11.0" +djangorestframework = "~=3.12.0" psycopg2-binary = "~=2.8.0" django-simple-bulma = "~=2.1" whitenoise = "~=5.0" -- cgit v1.2.3 From f65184b5e454437c3c524d1236041f170fff3ea4 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 14 Dec 2021 18:59:49 +0000 Subject: Query message count directly from messages This was changed due to performance reasons, but after some tweaking in the database, such as increasing work memory and adding an index, this query runs much faster now. To test this, I want to revert this change, so that we can stop the materialised view from refreshing, to see if the act of refreshing is what's causing this query to seem faster when runing against the database. --- pydis_site/apps/api/models/bot/metricity.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py index 52e946ac..901f191a 100644 --- a/pydis_site/apps/api/models/bot/metricity.py +++ b/pydis_site/apps/api/models/bot/metricity.py @@ -46,12 +46,14 @@ class Metricity: self.cursor.execute( """ SELECT - message_count - FROM user_has_approx_message_count + COUNT(*) + FROM messages WHERE author_id = '%s' + AND NOT is_deleted + AND channel_id NOT IN %s """, - [user_id] + [user_id, EXCLUDE_CHANNELS] ) values = self.cursor.fetchone() -- cgit v1.2.3 From 59e4a5c8316464e116630a329064decac4c2a075 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Thu, 16 Dec 2021 22:29:11 +0000 Subject: Always include metricity message blocks Thanks to a recent database maintenance (https://pythondiscord.freshstatus.io/incident/139811) querying out metricity message data is far cheaper. So there is no longer a reason to only fetch blocks if the member has a low message count. --- pydis_site/apps/api/tests/test_users.py | 21 +-------------------- pydis_site/apps/api/viewsets/bot/user.py | 6 +----- 2 files changed, 2 insertions(+), 25 deletions(-) diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py index 81bfd43b..9b91380b 100644 --- a/pydis_site/apps/api/tests/test_users.py +++ b/pydis_site/apps/api/tests/test_users.py @@ -408,7 +408,7 @@ class UserMetricityTests(AuthenticatedAPITestCase): in_guild=True, ) - def test_get_metricity_data_under_1k(self): + def test_get_metricity_data(self): # Given joined_at = "foo" total_messages = 1 @@ -428,25 +428,6 @@ class UserMetricityTests(AuthenticatedAPITestCase): "activity_blocks": total_blocks }) - def test_get_metricity_data_over_1k(self): - # Given - joined_at = "foo" - total_messages = 1001 - total_blocks = 1001 - self.mock_metricity_user(joined_at, total_messages, total_blocks, []) - - # When - url = reverse('api:bot:user-metricity-data', args=[0]) - response = self.client.get(url) - - # Then - self.assertEqual(response.status_code, 200) - self.assertCountEqual(response.json(), { - "joined_at": joined_at, - "total_messages": total_messages, - "voice_banned": False, - }) - def test_no_metricity_user(self): # Given self.mock_no_metricity_user() diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index ed661323..1a5e79f8 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -273,11 +273,7 @@ class UserViewSet(ModelViewSet): data = metricity.user(user.id) data["total_messages"] = metricity.total_messages(user.id) - if data["total_messages"] < 1000: - # Only calculate and return activity_blocks if the user has a small amount - # of messages, as calculating activity_blocks is expensive. - # 1000 message chosen as an arbitrarily large number. - data["activity_blocks"] = metricity.total_message_blocks(user.id) + data["activity_blocks"] = metricity.total_message_blocks(user.id) data["voice_banned"] = voice_banned return Response(data, status=status.HTTP_200_OK) -- cgit v1.2.3 From 6b0741c560ebe74e1003c5d19e2889d2d8fa8847 Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Fri, 31 Dec 2021 00:16:42 +0200 Subject: Fix faulty regex filters in migration 59 Migration 59 populated the filters table with regex filters, some of which were faulty because the `\b` character wasn't escaped. This is fixed in this PR by making all patterns raw strings. --- .../api/migrations/0059_populate_filterlists.py | 58 +++++++++++----------- 1 file changed, 29 insertions(+), 29 deletions(-) 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 = [ -- cgit v1.2.3 From 5c63524c8871144736147e3b136d633901d0ef9d Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 5 Jan 2022 15:13:52 +0000 Subject: Add netcup logo to page footer --- pydis_site/static/css/base/base.css | 8 ++++++++ pydis_site/templates/base/footer.html | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pydis_site/static/css/base/base.css b/pydis_site/static/css/base/base.css index f3fe1e44..340ff341 100644 --- a/pydis_site/static/css/base/base.css +++ b/pydis_site/static/css/base/base.css @@ -78,6 +78,14 @@ 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; 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 @@ -- cgit v1.2.3 From d5026f3f1ccbc8dd913d9aa58f9e6f41e5bc83e2 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 5 Jan 2022 15:15:29 +0000 Subject: Use an svg for django footer logo --- pydis_site/static/css/base/base.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pydis_site/static/css/base/base.css b/pydis_site/static/css/base/base.css index 340ff341..deac15d2 100644 --- a/pydis_site/static/css/base/base.css +++ b/pydis_site/static/css/base/base.css @@ -88,8 +88,8 @@ main.site-content { #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; color: #00000000; -- cgit v1.2.3 From 2384d6c4a739f5ee29bfd60c1608a67959169ba2 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 5 Jan 2022 15:15:48 +0000 Subject: Shift footer images slightly to be better in line --- pydis_site/static/css/base/base.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pydis_site/static/css/base/base.css b/pydis_site/static/css/base/base.css index deac15d2..4b36b7ce 100644 --- a/pydis_site/static/css/base/base.css +++ b/pydis_site/static/css/base/base.css @@ -91,7 +91,7 @@ main.site-content { 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; } @@ -100,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; } -- cgit v1.2.3 From 512b1659f5bb5d2dfb8029f42d7b8c7e3436ede1 Mon Sep 17 00:00:00 2001 From: Krish Date: Fri, 7 Jan 2022 12:19:29 +0530 Subject: Resource Suggestion: #639 solved --- .../pydis-guides/contributing/setting-test-server-and-bot-account.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 `` 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=&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 -- cgit v1.2.3 From 0bca620f5f540744d29df42c6cb68ed387815139 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 5 Jan 2022 15:16:08 +0000 Subject: Add netcup logo to sponsor block on homeapge --- pydis_site/static/images/sponsors/netcup.png | Bin 0 -> 9492 bytes pydis_site/templates/home/index.html | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 pydis_site/static/images/sponsors/netcup.png 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 Binary files /dev/null and b/pydis_site/static/images/sponsors/netcup.png differ 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
    + + netcup + Linode -- cgit v1.2.3 From da2c211fd7024edf3a27d7fafe9b110afbe705fc Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Fri, 14 Jan 2022 12:59:31 +0000 Subject: Add Django Discord community --- pydis_site/apps/resources/resources/communities/django.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 pydis_site/apps/resources/resources/communities/django.yaml diff --git a/pydis_site/apps/resources/resources/communities/django.yaml b/pydis_site/apps/resources/resources/communities/django.yaml new file mode 100644 index 00000000..0ef729ba --- /dev/null +++ b/pydis_site/apps/resources/resources/communities/django.yaml @@ -0,0 +1,13 @@ +description: Django is a high-level Python web framework that encourages rapid development and clean, + pragmatic design. Built by experienced developers, it takes care of much of the hassle of web development, + so you can focus on writing your app without needing to reinvent the wheel. It’s free and open source. +title_image: https://static.djangoproject.com/img/logos/django-logo-positive.png +title_url: https://discord.gg/M9Mx3RvKM5 +position: 5 +urls: + - icon: branding/discord + url: https://discord.gg/M9Mx3RvKM5 + color: blurple + - icon: regular/link + url: https://www.djangoproject.com/ + color: teal -- cgit v1.2.3 From 71a4214f53db5fc4c09fba9715f8f87cfa487123 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Fri, 14 Jan 2022 12:59:46 +0000 Subject: Renumber communities after Django --- pydis_site/apps/resources/resources/communities/kivy.yaml | 2 +- pydis_site/apps/resources/resources/communities/pallets.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pydis_site/apps/resources/resources/communities/kivy.yaml b/pydis_site/apps/resources/resources/communities/kivy.yaml index 601d7dba..69f117c7 100644 --- a/pydis_site/apps/resources/resources/communities/kivy.yaml +++ b/pydis_site/apps/resources/resources/communities/kivy.yaml @@ -5,7 +5,7 @@ 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 +position: 6 urls: - icon: branding/discord url: https://discord.gg/djPtTRJ diff --git a/pydis_site/apps/resources/resources/communities/pallets.yaml b/pydis_site/apps/resources/resources/communities/pallets.yaml index 239b1491..64c97dab 100644 --- a/pydis_site/apps/resources/resources/communities/pallets.yaml +++ b/pydis_site/apps/resources/resources/communities/pallets.yaml @@ -3,7 +3,7 @@ description: The Pallets Projects develop Python libraries such as the Flask web and get help from the Pallets community. title_image: https://i.imgur.com/sV9Ypdf.png title_url: https://discord.gg/t6rrQZH -position: 6 +position: 7 urls: - icon: branding/discord url: https://discord.gg/t6rrQZH -- cgit v1.2.3