aboutsummaryrefslogtreecommitdiffstats
path: root/pydis_site
diff options
context:
space:
mode:
Diffstat (limited to 'pydis_site')
-rw-r--r--pydis_site/apps/api/migrations/0069_documentationlink_validators.py25
-rw-r--r--pydis_site/apps/api/models/bot/documentation_link.py17
-rw-r--r--pydis_site/apps/api/pagination.py49
-rw-r--r--pydis_site/apps/api/tests/test_documentation_links.py15
-rw-r--r--pydis_site/apps/api/viewsets/bot/infraction.py5
-rw-r--r--pydis_site/templates/home/index.html2
6 files changed, 109 insertions, 4 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',)
diff --git a/pydis_site/templates/home/index.html b/pydis_site/templates/home/index.html
index 67f29e41..18f6b77b 100644
--- a/pydis_site/templates/home/index.html
+++ b/pydis_site/templates/home/index.html
@@ -82,7 +82,7 @@
</p>
<p>
You can find help with most Python-related problems in one of our help channels.
- Our staff of over 90 dedicated expert Helpers are available around the clock
+ Our staff of over 100 dedicated expert Helpers are available around the clock
in every timezone. Whether you're looking to learn the language or working on a
complex project, we've got someone who can help you if you get stuck.
</p>