diff options
Diffstat (limited to 'pydis_site/apps')
| -rw-r--r-- | pydis_site/apps/api/migrations/0095_user_display_name.py | 18 | ||||
| -rw-r--r-- | pydis_site/apps/api/models/bot/user.py | 5 | ||||
| -rw-r--r-- | pydis_site/apps/api/serializers.py | 4 | ||||
| -rw-r--r-- | pydis_site/apps/api/tests/test_filters.py | 47 | ||||
| -rw-r--r-- | pydis_site/apps/api/tests/test_github_webhook_filter.py | 23 | ||||
| -rw-r--r-- | pydis_site/apps/api/tests/test_users.py | 13 | ||||
| -rw-r--r-- | pydis_site/apps/api/views.py | 23 | ||||
| -rw-r--r-- | pydis_site/apps/api/viewsets/bot/user.py | 14 | ||||
| -rw-r--r-- | pydis_site/apps/content/resources/guides/pydis-guides/contributing/bot.md | 2 | ||||
| -rw-r--r-- | pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot.md | 2 | ||||
| -rw-r--r-- | pydis_site/apps/content/tests/test_utils.py | 12 | ||||
| -rw-r--r-- | pydis_site/apps/content/utils.py | 31 | 
12 files changed, 159 insertions, 35 deletions
diff --git a/pydis_site/apps/api/migrations/0095_user_display_name.py b/pydis_site/apps/api/migrations/0095_user_display_name.py new file mode 100644 index 00000000..82381830 --- /dev/null +++ b/pydis_site/apps/api/migrations/0095_user_display_name.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.3 on 2024-04-01 12:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): +    dependencies = [ +        ("api", "0094_migrate_mailing_listdata"), +    ] + +    operations = [ +        migrations.AddField( +            model_name="user", +            name="display_name", +            field=models.CharField(blank=True, help_text="The display name, taken from Discord.", max_length=32), +        ), +        migrations.RunSQL("UPDATE api_user SET display_name = name;", elidable=True), +    ] diff --git a/pydis_site/apps/api/models/bot/user.py b/pydis_site/apps/api/models/bot/user.py index afc5ba1e..1cb10988 100644 --- a/pydis_site/apps/api/models/bot/user.py +++ b/pydis_site/apps/api/models/bot/user.py @@ -33,6 +33,11 @@ class User(ModelReprMixin, models.Model):          max_length=32,          help_text="The username, taken from Discord.",      ) +    display_name = models.CharField( +        max_length=32, +        blank=True, +        help_text="The display name, taken from Discord.", +    )      discriminator = models.PositiveSmallIntegerField(          validators=(              MaxValueValidator( diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index ea94214f..60d3637c 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -434,7 +434,7 @@ class FilterListSerializer(ModelSerializer):          schema = {name: getattr(instance, name) for name in BASE_FILTERLIST_FIELDS}          schema['filters'] = [              FilterSerializer(many=False).to_representation(instance=item) -            for item in Filter.objects.filter(filter_list=instance.id) +            for item in Filter.objects.filter(filter_list=instance.id).prefetch_related('filter_list')          ]          settings = {name: getattr(instance, name) for name in BASE_SETTINGS_FIELDS} @@ -673,7 +673,7 @@ class UserSerializer(ModelSerializer):          """Metadata defined for the Django REST Framework."""          model = User -        fields = ('id', 'name', 'discriminator', 'roles', 'in_guild') +        fields = ('id', 'name', 'display_name', 'discriminator', 'roles', 'in_guild')          depth = 1          list_serializer_class = UserListSerializer diff --git a/pydis_site/apps/api/tests/test_filters.py b/pydis_site/apps/api/tests/test_filters.py index 4cef1c8f..96b3a65c 100644 --- a/pydis_site/apps/api/tests/test_filters.py +++ b/pydis_site/apps/api/tests/test_filters.py @@ -6,7 +6,7 @@ from typing import Any  from django.db.models import Model  from django.urls import reverse -from pydis_site.apps.api.models.bot.filters import Filter, FilterList +from pydis_site.apps.api.models.bot.filters import Filter, FilterList, FilterListType  from pydis_site.apps.api.tests.base import AuthenticatedAPITestCase @@ -350,3 +350,48 @@ class FilterValidationTests(AuthenticatedAPITestCase):          response = self.client.post(test_filter.url(), data=clean_test_json(test_filter.object))          self.assertEqual(response.status_code, 400) + + +class FilterCreationMissingOptionalFieldsTestCase(AuthenticatedAPITestCase): +    @classmethod +    def setUpTestData(cls): +        cls.filter_list = FilterList.objects.create( +            name="Ingsoc", +            list_type=FilterListType.ALLOW, +            dm_content="But if thought corrupts language, language can also corrupt thought.", +            dm_embed="", +            infraction_type="timeout", +            infraction_duration=timedelta(days=80 * 365), +            infraction_reason="Thoughtcrime", +            infraction_channel=1, +            guild_pings=["@BigBrother"], +            filter_dm=False, +            dm_pings=["@BigBrother"], +            remove_context=True, +            bypass_roles=[], +            enabled=True, +            send_alert=True, +            enabled_channels=[], +            disabled_channels=[], +            enabled_categories=[], +            disabled_categories=[], +        ) + +    def test_creation_missing_optional_fields(self) -> None: +        data = { +            "filter_list": self.filter_list.id, +            "content": "1234567", +            "description": "Guild \"Python\" - Phishing", +            "additional_settings": {}, +            "guild_pings": [], +            "infraction_type": "BAN", +            "infraction_channel": 1, +            "infraction_duration": 345600.0, +            "infraction_reason": ( +                "The creatures outside looked from pig to man, and from man to pig, " +                "and from pig to man again; but already it was impossible to say which was which" +            ) +        } +        endpoint = reverse('api:bot:filter-list') +        response = self.client.post(endpoint, data=data) +        self.assertEqual(response.status_code, 201) diff --git a/pydis_site/apps/api/tests/test_github_webhook_filter.py b/pydis_site/apps/api/tests/test_github_webhook_filter.py index 8ca60511..d64e1a13 100644 --- a/pydis_site/apps/api/tests/test_github_webhook_filter.py +++ b/pydis_site/apps/api/tests/test_github_webhook_filter.py @@ -1,3 +1,4 @@ +import io  from unittest import mock  from urllib.error import HTTPError @@ -44,8 +45,10 @@ class GitHubWebhookFilterAPITests(APITestCase):              context_mock.read.return_value = b'{"status": "ok"}'              response = self.client.post(url, data=payload, headers=headers) -            self.assertEqual(response.status_code, context_mock.status) -            self.assertEqual(response.headers.get('X-Clacks-Overhead'), 'Joe Armstrong') +            response_body = response.json() +            self.assertEqual(response.status_code, 200) +            self.assertEqual(response_body.get("headers", {}).get("X-Clacks-Overhead"), 'Joe Armstrong') +            self.assertEqual(response_body.get("original_status"), 299)      def test_rate_limit_is_logged_to_sentry(self):          url = reverse('api:github-webhook-filter', args=('id', 'token')) @@ -56,6 +59,22 @@ class GitHubWebhookFilterAPITests(APITestCase):              mock.patch.object(GitHubWebhookFilterView, "logger") as logger,          ):              urlopen.side_effect = HTTPError(None, 429, 'Too Many Requests', {}, None) +            urlopen.side_effect.fp = io.BytesIO() +            logger.warning = mock.PropertyMock() +            self.client.post(url, data=payload, headers=headers) + +            logger.warning.assert_called_once() + +    def test_other_error_is_logged(self): +        url = reverse('api:github-webhook-filter', args=('id', 'token')) +        payload = {} +        headers = {'X-GitHub-Event': 'pull_request_review'} +        with ( +            mock.patch('urllib.request.urlopen') as urlopen, +            mock.patch.object(GitHubWebhookFilterView, "logger") as logger, +        ): +            urlopen.side_effect = HTTPError(None, 451, 'Unavailable For Legal Reasons', {}, None) +            urlopen.side_effect.fp = io.BytesIO()              logger.warning = mock.PropertyMock()              self.client.post(url, data=payload, headers=headers) diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py index cff4a825..5dda6344 100644 --- a/pydis_site/apps/api/tests/test_users.py +++ b/pydis_site/apps/api/tests/test_users.py @@ -61,7 +61,8 @@ class CreationTests(AuthenticatedAPITestCase):          url = reverse('api:bot:user-list')          data = {              'id': 42, -            'name': "Test", +            'name': "test", +            'display_name': "Test Display",              'discriminator': 42,              'roles': [                  self.role.id @@ -75,6 +76,7 @@ class CreationTests(AuthenticatedAPITestCase):          user = User.objects.get(id=42)          self.assertEqual(user.name, data['name']) +        self.assertEqual(user.display_name, data['display_name'])          self.assertEqual(user.discriminator, data['discriminator'])          self.assertEqual(user.in_guild, data['in_guild']) @@ -83,7 +85,8 @@ class CreationTests(AuthenticatedAPITestCase):          data = [              {                  'id': 5, -                'name': "test man", +                'name': "testman", +                'display_name': "Test Display 1",                  'discriminator': 42,                  'roles': [                      self.role.id @@ -92,7 +95,8 @@ class CreationTests(AuthenticatedAPITestCase):              },              {                  'id': 8, -                'name': "another test man", +                'name': "anothertestman", +                'display_name': "Test Display 2",                  'discriminator': 555,                  'roles': [],                  'in_guild': False @@ -200,7 +204,8 @@ class MultiPatchTests(AuthenticatedAPITestCase):          data = [              {                  "id": 1, -                "name": "User 1 patched!", +                "name": "user1patched", +                "display_name": "User 1 Patched",                  "discriminator": 1010,                  "roles": [self.role_developer.id],                  "in_guild": False diff --git a/pydis_site/apps/api/views.py b/pydis_site/apps/api/views.py index 1fa3efc2..a3b0016c 100644 --- a/pydis_site/apps/api/views.py +++ b/pydis_site/apps/api/views.py @@ -303,9 +303,26 @@ class GitHubWebhookFilterView(APIView):          (response_status, headers, body) = self.send_webhook(              webhook_id, webhook_token, request.data, dict(request.headers),          ) -        headers.pop('Connection', None) -        headers.pop('Content-Length', None) -        return Response(data=body, headers=headers, status=response_status) + +        body_decoded = body.decode("utf-8") + +        if ( +            not (status.HTTP_200_OK <= response_status < status.HTTP_300_MULTIPLE_CHOICES) +            and response_status != status.HTTP_429_TOO_MANY_REQUESTS +        ): +            self.logger.warning( +                "Failed to send GitHub webhook to Discord. Response code %d, body: %s", +                response_status, +                body_decoded, +            ) + +        response_body = { +            "original_status": response_status, +            "data": body_decoded, +            "headers": headers, +        } + +        return Response(response_body)      def send_webhook(          self, diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 77378336..d06eb868 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -64,7 +64,8 @@ class UserViewSet(ModelViewSet):      ...     'results': [      ...      {      ...         'id': 409107086526644234, -    ...         'name': "Python", +    ...         'name': "python", +    ...         'display_name': "Python",      ...         'discriminator': 4329,      ...         'roles': [      ...             352427296948486144, @@ -79,6 +80,7 @@ class UserViewSet(ModelViewSet):      #### Optional Query Parameters      - username: username to search for +    - display_name: display name to search for      - discriminator: discriminator to search for      - page_size: number of Users in one page, defaults to 10,000      - page: page number @@ -92,7 +94,8 @@ class UserViewSet(ModelViewSet):      #### Response format      >>> {      ...     'id': 409107086526644234, -    ...     'name': "Python", +    ...     'name': "python", +    ...     'display_name': "Python",      ...     'discriminator': 4329,      ...     'roles': [      ...         352427296948486144, @@ -170,6 +173,7 @@ class UserViewSet(ModelViewSet):      >>> {      ...     'id': int,      ...     'name': str, +    ...     'display_name': str,      ...     'discriminator': int,      ...     'roles': List[int],      ...     'in_guild': bool @@ -192,6 +196,7 @@ class UserViewSet(ModelViewSet):      >>> {      ...     'id': int,      ...     'name': str, +    ...     'display_name': str,      ...     'discriminator': int,      ...     'roles': List[int],      ...     'in_guild': bool @@ -210,6 +215,7 @@ class UserViewSet(ModelViewSet):      >>> {      ...     'id': int,      ...     'name': str, +    ...     'display_name': str,      ...     'discriminator': int,      ...     'roles': List[int],      ...     'in_guild': bool @@ -229,6 +235,7 @@ class UserViewSet(ModelViewSet):      ...     {      ...         'id': int,      ...         'name': str, +    ...         'display_name': str,      ...         'discriminator': int,      ...         'roles': List[int],      ...         'in_guild': bool @@ -236,6 +243,7 @@ class UserViewSet(ModelViewSet):      ...     {      ...         'id': int,      ...         'name': str, +    ...         'display_name': str,      ...         'discriminator': int,      ...         'roles': List[int],      ...         'in_guild': bool @@ -260,7 +268,7 @@ class UserViewSet(ModelViewSet):      queryset = User.objects.all().order_by("id")      pagination_class = UserListPagination      filter_backends = (DjangoFilterBackend,) -    filterset_fields = ('name', 'discriminator') +    filterset_fields = ('name', 'discriminator', 'display_name')      def get_serializer(self, *args, **kwargs) -> ModelSerializer:          """Set Serializer many attribute to True if request body contains a list.""" 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 f54ee664..a414bc20 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 @@ -15,7 +15,7 @@ This page will focus on the quickest steps one can take, with mentions of altern  ### Setup Project Dependencies  Below are the dependencies you **must** have installed to get started with the bot. -1. Make sure you have [Python 3.11](https://www.python.org/downloads/) installed. It helps if it is your system's default Python version. +1. Make sure you have [Python 3.12](https://www.python.org/downloads/) installed. It helps if it is your system's default Python version.  1. [Install Poetry](https://github.com/python-poetry/poetry#installation).  1. [Install the project's dependencies](../installing-project-dependencies).  1. Docker. 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 7861c3d9..56d95db4 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 @@ -10,7 +10,7 @@ You should have already forked the [`sir-lancebot`](https://github.com/python-di  Remember to ensure that you have read the [contributing guidelines](../contributing-guidelines) in full before you start contributing.  ### Requirements -- [Python 3.10.*](https://www.python.org/downloads/) +- [Python 3.12.*](https://www.python.org/downloads/)  - [Poetry](https://github.com/python-poetry/poetry#installation)  - [Git](https://git-scm.com/downloads)      - [Windows Installer](https://git-scm.com/download/win) diff --git a/pydis_site/apps/content/tests/test_utils.py b/pydis_site/apps/content/tests/test_utils.py index 7f7736f9..d26c59d5 100644 --- a/pydis_site/apps/content/tests/test_utils.py +++ b/pydis_site/apps/content/tests/test_utils.py @@ -370,7 +370,7 @@ class TagUtilsTests(TestCase):          self.assertEqual(self.commit, models.Tag.objects.get(name=tag.name).last_commit)      @mock.patch.object(utils, "set_tag_commit") -    def test_exiting_commit(self, set_commit_mock: mock.Mock): +    def test_existing_commit(self, set_commit_mock: mock.Mock):          """Test that a commit is saved when the data has not changed."""          tag = models.Tag.objects.create(name="tag-name", body="old body", last_commit=self.commit) @@ -378,8 +378,18 @@ class TagUtilsTests(TestCase):          tag.last_commit = None          utils.record_tags([tag]) +        tag.refresh_from_db()          self.assertEqual(self.commit, tag.last_commit)          result = utils.get_tag("tag-name")          self.assertEqual(tag, result)          set_commit_mock.assert_not_called() + +    def test_deletes_tags_no_longer_present(self): +        """Test that no longer known tags are deleted.""" +        tag = models.Tag.objects.create(name="tag-name", body="old body", last_commit=self.commit) + +        utils.record_tags([]) + +        with self.assertRaises(models.Tag.DoesNotExist): +            tag.refresh_from_db() diff --git a/pydis_site/apps/content/utils.py b/pydis_site/apps/content/utils.py index 5a146e10..720063e4 100644 --- a/pydis_site/apps/content/utils.py +++ b/pydis_site/apps/content/utils.py @@ -12,6 +12,7 @@ import frontmatter  import httpx  import markdown  import yaml +from django.db import transaction  from django.http import Http404  from django.utils import timezone  from markdown.extensions.toc import TocExtension @@ -194,23 +195,19 @@ def set_tag_commit(tag: Tag) -> None:  def record_tags(tags: list[Tag]) -> None:      """Sync the database with an updated set of tags.""" -    # Remove entries which no longer exist -    Tag.objects.exclude(name__in=[tag.name for tag in tags]).delete() - -    # Insert/update the tags -    for new_tag in tags: -        try: -            old_tag = Tag.objects.get(name=new_tag.name) -        except Tag.DoesNotExist: -            # The tag is not in the database yet, -            # pretend it's previous state is the current state -            old_tag = new_tag - -        if old_tag.sha == new_tag.sha and old_tag.last_commit_id is not None: -            # We still have an up-to-date commit entry -            new_tag.last_commit_id = old_tag.last_commit_id - -        new_tag.save() +    with transaction.atomic(): +        # Remove any tags that we don't want to keep in the future +        Tag.objects.exclude(name__in=(tag.name for tag in tags)).delete() + +        # Upsert the data! +        Tag.objects.bulk_create( +            tags, +            update_conflicts=True, +            # last_commit is not included here. We want to keep that +            # from the tag that might already be in the database. +            update_fields=('last_updated', 'sha', 'group', 'body'), +            unique_fields=('name',), +        )      # Drop old, unused commits      Commit.objects.filter(tag__isnull=True).delete()  |