diff options
Diffstat (limited to 'pydis_site/apps')
| -rw-r--r-- | pydis_site/apps/api/migrations/0069_documentationlink_validators.py | 25 | ||||
| -rw-r--r-- | pydis_site/apps/api/models/bot/documentation_link.py | 17 | ||||
| -rw-r--r-- | pydis_site/apps/api/pagination.py | 49 | ||||
| -rw-r--r-- | pydis_site/apps/api/tests/test_documentation_links.py | 15 | ||||
| -rw-r--r-- | pydis_site/apps/api/viewsets/bot/infraction.py | 5 | 
5 files changed, 108 insertions, 3 deletions
diff --git a/pydis_site/apps/api/migrations/0069_documentationlink_validators.py b/pydis_site/apps/api/migrations/0069_documentationlink_validators.py new file mode 100644 index 00000000..347c0e1a --- /dev/null +++ b/pydis_site/apps/api/migrations/0069_documentationlink_validators.py @@ -0,0 +1,25 @@ +# Generated by Django 3.0.11 on 2021-03-26 18:21 + +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.documentation_link + + +class Migration(migrations.Migration): + +    dependencies = [ +        ('api', '0068_split_nomination_tables'), +    ] + +    operations = [ +        migrations.AlterField( +            model_name='documentationlink', +            name='base_url', +            field=models.URLField(help_text='The base URL from which documentation will be available for this project. Used to generate links to various symbols within this package.', validators=[pydis_site.apps.api.models.bot.documentation_link.ends_with_slash_validator]), +        ), +        migrations.AlterField( +            model_name='documentationlink', +            name='package', +            field=models.CharField(help_text='The Python package name that this documentation link belongs to.', max_length=50, primary_key=True, serialize=False, validators=[django.core.validators.RegexValidator(message='Package names can only consist of lowercase a-z letters, digits, and underscores.', regex='^[a-z0-9_]+$')]), +        ), +    ] diff --git a/pydis_site/apps/api/models/bot/documentation_link.py b/pydis_site/apps/api/models/bot/documentation_link.py index 2a0ce751..3dcc71fc 100644 --- a/pydis_site/apps/api/models/bot/documentation_link.py +++ b/pydis_site/apps/api/models/bot/documentation_link.py @@ -1,7 +1,20 @@ +from django.core.exceptions import ValidationError +from django.core.validators import RegexValidator  from django.db import models  from pydis_site.apps.api.models.mixins import ModelReprMixin +package_name_validator = RegexValidator( +    regex=r"^[a-z0-9_]+$", +    message="Package names can only consist of lowercase a-z letters, digits, and underscores." +) + + +def ends_with_slash_validator(string: str) -> None: +    """Raise a ValidationError if `string` does not end with a slash.""" +    if not string.endswith("/"): +        raise ValidationError("The entered URL must end with a slash.") +  class DocumentationLink(ModelReprMixin, models.Model):      """A documentation link used by the `!docs` command of the bot.""" @@ -9,13 +22,15 @@ class DocumentationLink(ModelReprMixin, models.Model):      package = models.CharField(          primary_key=True,          max_length=50, +        validators=(package_name_validator,),          help_text="The Python package name that this documentation link belongs to."      )      base_url = models.URLField(          help_text=(              "The base URL from which documentation will be available for this project. "              "Used to generate links to various symbols within this package." -        ) +        ), +        validators=(ends_with_slash_validator,)      )      inventory_url = models.URLField(          help_text="The URL at which the Sphinx inventory is available for this package." diff --git a/pydis_site/apps/api/pagination.py b/pydis_site/apps/api/pagination.py new file mode 100644 index 00000000..2a325460 --- /dev/null +++ b/pydis_site/apps/api/pagination.py @@ -0,0 +1,49 @@ +import typing + +from rest_framework.pagination import LimitOffsetPagination +from rest_framework.response import Response + + +class LimitOffsetPaginationExtended(LimitOffsetPagination): +    """ +    Extend LimitOffsetPagination to customise the default response. + +    For example: + +    ## Default response +    >>> { +    ...     "count": 1, +    ...     "next": None, +    ...     "previous": None, +    ...     "results": [{ +    ...         "id": 6, +    ...         "inserted_at": "2021-01-26T21:13:35.477879Z", +    ...         "expires_at": None, +    ...         "active": False, +    ...         "user": 1, +    ...         "actor": 2, +    ...         "type": "warning", +    ...         "reason": null, +    ...         "hidden": false +    ...     }] +    ... } + +    ## Required response +    >>> [{ +    ...     "id": 6, +    ...     "inserted_at": "2021-01-26T21:13:35.477879Z", +    ...     "expires_at": None, +    ...     "active": False, +    ...     "user": 1, +    ...     "actor": 2, +    ...     "type": "warning", +    ...     "reason": None, +    ...     "hidden": False +    ... }] +    """ + +    default_limit = 100 + +    def get_paginated_response(self, data: typing.Any) -> Response: +        """Override to skip metadata i.e. `count`, `next`, and `previous`.""" +        return Response(data) diff --git a/pydis_site/apps/api/tests/test_documentation_links.py b/pydis_site/apps/api/tests/test_documentation_links.py index e560a2fd..39fb08f3 100644 --- a/pydis_site/apps/api/tests/test_documentation_links.py +++ b/pydis_site/apps/api/tests/test_documentation_links.py @@ -60,7 +60,7 @@ class DetailLookupDocumentationLinkAPITests(APISubdomainTestCase):      def setUpTestData(cls):          cls.doc_link = DocumentationLink.objects.create(              package='testpackage', -            base_url='https://example.com', +            base_url='https://example.com/',              inventory_url='https://example.com'          ) @@ -108,6 +108,17 @@ class DetailLookupDocumentationLinkAPITests(APISubdomainTestCase):          self.assertEqual(response.status_code, 400) +    def test_create_invalid_package_name_returns_400(self): +        test_cases = ("InvalidPackage", "invalid package", "i\u0150valid") +        for case in test_cases: +            with self.subTest(package_name=case): +                body = self.doc_json.copy() +                body['package'] = case +                url = reverse('bot:documentationlink-list', host='api') +                response = self.client.post(url, data=body) + +                self.assertEqual(response.status_code, 400) +  class DocumentationLinkCreationTests(APISubdomainTestCase):      def setUp(self): @@ -115,7 +126,7 @@ class DocumentationLinkCreationTests(APISubdomainTestCase):          self.body = {              'package': 'example', -            'base_url': 'https://example.com', +            'base_url': 'https://example.com/',              'inventory_url': 'https://docs.example.com'          } diff --git a/pydis_site/apps/api/viewsets/bot/infraction.py b/pydis_site/apps/api/viewsets/bot/infraction.py index 423e806e..bd512ddd 100644 --- a/pydis_site/apps/api/viewsets/bot/infraction.py +++ b/pydis_site/apps/api/viewsets/bot/infraction.py @@ -13,6 +13,7 @@ from rest_framework.response import Response  from rest_framework.viewsets import GenericViewSet  from pydis_site.apps.api.models.bot.infraction import Infraction +from pydis_site.apps.api.pagination import LimitOffsetPaginationExtended  from pydis_site.apps.api.serializers import (      ExpandedInfractionSerializer,      InfractionSerializer @@ -38,6 +39,8 @@ class InfractionViewSet(      - **active** `bool`: whether the infraction is still active      - **actor__id** `int`: snowflake of the user which applied the infraction      - **hidden** `bool`: whether the infraction is a shadow infraction +    - **limit** `int`: number of results return per page (default 100) +    - **offset** `int`: the initial index from which to return the results (default 0)      - **search** `str`: regular expression applied to the infraction's reason      - **type** `str`: the type of the infraction      - **user__id** `int`: snowflake of the user to which the infraction was applied @@ -46,6 +49,7 @@ class InfractionViewSet(      Invalid query parameters are ignored.      #### Response format +    Response is paginated but the result is returned without any pagination metadata.      >>> [      ...     {      ...         'id': 5, @@ -133,6 +137,7 @@ class InfractionViewSet(      serializer_class = InfractionSerializer      queryset = Infraction.objects.all() +    pagination_class = LimitOffsetPaginationExtended      filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter)      filter_fields = ('user__id', 'actor__id', 'active', 'hidden', 'type')      search_fields = ('$reason',)  |