aboutsummaryrefslogtreecommitdiffstats
path: root/pydis_site/apps
diff options
context:
space:
mode:
authorGravatar Johannes Christ <[email protected]>2021-11-26 20:33:33 +0100
committerGravatar GitHub <[email protected]>2021-11-26 20:33:33 +0100
commit80d355d3f3b9d4950d3bd42dbc2d79b55b1b886e (patch)
tree2f47a75ef571a8a7fe3b2e97142810fd9358630a /pydis_site/apps
parentFix gitpod link in Sir Lancebot contributing guide (diff)
parentMerge pull request #612 from python-discord/redirects (diff)
Merge branch 'main' into patch-1
Diffstat (limited to 'pydis_site/apps')
-rw-r--r--pydis_site/apps/api/migrations/0074_reminder_failures.py18
-rw-r--r--pydis_site/apps/api/migrations/0075_add_redirects_filter.py18
-rw-r--r--pydis_site/apps/api/migrations/0075_infraction_dm_sent.py18
-rw-r--r--pydis_site/apps/api/migrations/0076_merge_20211125_1941.py14
-rw-r--r--pydis_site/apps/api/models/bot/filter_list.py1
-rw-r--r--pydis_site/apps/api/models/bot/infraction.py4
-rw-r--r--pydis_site/apps/api/models/bot/metricity.py14
-rw-r--r--pydis_site/apps/api/models/bot/reminder.py4
-rw-r--r--pydis_site/apps/api/serializers.py21
-rw-r--r--pydis_site/apps/api/tests/test_users.py23
-rw-r--r--pydis_site/apps/api/viewsets/bot/filter_list.py3
-rw-r--r--pydis_site/apps/api/viewsets/bot/infraction.py9
-rw-r--r--pydis_site/apps/api/viewsets/bot/reminder.py13
-rw-r--r--pydis_site/apps/api/viewsets/bot/user.py8
-rw-r--r--pydis_site/apps/redirect/redirects.yaml2
-rw-r--r--pydis_site/apps/redirect/urls.py114
16 files changed, 248 insertions, 36 deletions
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/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/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/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 = [
+ ]
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/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/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;
""",
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..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'
+ 'id',
+ 'inserted_at',
+ 'expires_at',
+ 'active',
+ 'user',
+ 'actor',
+ 'type',
+ 'reason',
+ 'hidden',
+ 'dm_sent'
)
validators = [
UniqueTogetherValidator(
@@ -231,7 +240,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/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py
index 295bcf64..81bfd43b 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,13 +421,32 @@ 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,
"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/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
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
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/<guild_id>/<channel_id>/<message_id>"
+ ... 'jump_url': "https://discord.com/channels/<guild_id>/<channel_id>/<message_id>",
+ ... '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/<guild_id>/<channel_id>/<message_id>"
+ ... 'jump_url': "https://discord.com/channels/<guild_id>/<channel_id>/<message_id>",
+ ... '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
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"),
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 = "<meta http-equiv=\"refresh\" content=\"0; URL={url}\"/>"
+
+
[email protected](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)))