aboutsummaryrefslogtreecommitdiffstats
path: root/pydis_site
diff options
context:
space:
mode:
Diffstat (limited to 'pydis_site')
-rw-r--r--pydis_site/apps/api/admin.py2
-rw-r--r--pydis_site/apps/api/migrations/0008_tag_embed_validator.py7
-rw-r--r--pydis_site/apps/api/migrations/0019_deletedmessage.py2
-rw-r--r--pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py4
-rw-r--r--pydis_site/apps/api/migrations/0051_delete_tag.py16
-rw-r--r--pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py18
-rw-r--r--pydis_site/apps/api/migrations/0061_merge_20200830_0526.py14
-rw-r--r--pydis_site/apps/api/migrations/0062_merge_20200901_1459.py14
-rw-r--r--pydis_site/apps/api/models/__init__.py1
-rw-r--r--pydis_site/apps/api/models/bot/__init__.py1
-rw-r--r--pydis_site/apps/api/models/bot/message.py4
-rw-r--r--pydis_site/apps/api/models/bot/off_topic_channel_name.py5
-rw-r--r--pydis_site/apps/api/models/utils.py (renamed from pydis_site/apps/api/models/bot/tag.py)43
-rw-r--r--pydis_site/apps/api/serializers.py11
-rw-r--r--pydis_site/apps/api/tests/test_models.py5
-rw-r--r--pydis_site/apps/api/tests/test_off_topic_channel_names.py27
-rw-r--r--pydis_site/apps/api/tests/test_validators.py56
-rw-r--r--pydis_site/apps/api/urls.py5
-rw-r--r--pydis_site/apps/api/viewsets/__init__.py1
-rw-r--r--pydis_site/apps/api/viewsets/bot/__init__.py1
-rw-r--r--pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py27
-rw-r--r--pydis_site/apps/api/viewsets/bot/tag.py105
-rw-r--r--pydis_site/apps/home/tests/mock_github_api_response.json2
-rw-r--r--pydis_site/apps/home/views/home.py2
-rw-r--r--pydis_site/constants.py5
-rw-r--r--pydis_site/context_processors.py8
-rw-r--r--pydis_site/settings.py6
-rw-r--r--pydis_site/templates/base/base.html1
28 files changed, 179 insertions, 214 deletions
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py
index 0333fefc..dd1291b8 100644
--- a/pydis_site/apps/api/admin.py
+++ b/pydis_site/apps/api/admin.py
@@ -14,7 +14,6 @@ from .models import (
OffTopicChannelName,
OffensiveMessage,
Role,
- Tag,
User
)
@@ -64,5 +63,4 @@ admin.site.register(Nomination)
admin.site.register(OffensiveMessage)
admin.site.register(OffTopicChannelName)
admin.site.register(Role)
-admin.site.register(Tag)
admin.site.register(User)
diff --git a/pydis_site/apps/api/migrations/0008_tag_embed_validator.py b/pydis_site/apps/api/migrations/0008_tag_embed_validator.py
index d53ddb90..d92042d2 100644
--- a/pydis_site/apps/api/migrations/0008_tag_embed_validator.py
+++ b/pydis_site/apps/api/migrations/0008_tag_embed_validator.py
@@ -1,7 +1,5 @@
# Generated by Django 2.1.1 on 2018-09-23 10:07
-import pydis_site.apps.api.models.bot.tag
-import django.contrib.postgres.fields.jsonb
from django.db import migrations
@@ -12,9 +10,4 @@ class Migration(migrations.Migration):
]
operations = [
- migrations.AlterField(
- model_name='tag',
- name='embed',
- field=django.contrib.postgres.fields.jsonb.JSONField(help_text='The actual embed shown by this tag.', validators=[pydis_site.apps.api.models.bot.tag.validate_tag_embed]),
- ),
]
diff --git a/pydis_site/apps/api/migrations/0019_deletedmessage.py b/pydis_site/apps/api/migrations/0019_deletedmessage.py
index 33746253..6b848d64 100644
--- a/pydis_site/apps/api/migrations/0019_deletedmessage.py
+++ b/pydis_site/apps/api/migrations/0019_deletedmessage.py
@@ -18,7 +18,7 @@ class Migration(migrations.Migration):
('id', models.BigIntegerField(help_text='The message ID as taken from Discord.', primary_key=True, serialize=False, validators=[django.core.validators.MinValueValidator(limit_value=0, message='Message IDs cannot be negative.')])),
('channel_id', models.BigIntegerField(help_text='The channel ID that this message was sent in, taken from Discord.', validators=[django.core.validators.MinValueValidator(limit_value=0, message='Channel IDs cannot be negative.')])),
('content', models.CharField(help_text='The content of this message, taken from Discord.', max_length=2000)),
- ('embeds', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(validators=[pydis_site.apps.api.models.bot.tag.validate_tag_embed]), help_text='Embeds attached to this message.', size=None)),
+ ('embeds', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(validators=[pydis_site.apps.api.models.utils.validate_embed]), help_text='Embeds attached to this message.', size=None)),
('author', models.ForeignKey(help_text='The author of this message.', on_delete=django.db.models.deletion.CASCADE, to='api.User')),
('deletion_context', models.ForeignKey(help_text='The deletion context this message is part of.', on_delete=django.db.models.deletion.CASCADE, to='api.MessageDeletionContext')),
],
diff --git a/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py b/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py
index e617e1c9..124c6a57 100644
--- a/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py
+++ b/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py
@@ -3,7 +3,7 @@
import django.contrib.postgres.fields
import django.contrib.postgres.fields.jsonb
from django.db import migrations
-import pydis_site.apps.api.models.bot.tag
+import pydis_site.apps.api.models.utils
class Migration(migrations.Migration):
@@ -16,6 +16,6 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='deletedmessage',
name='embeds',
- field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(validators=[pydis_site.apps.api.models.bot.tag.validate_tag_embed]), blank=True, help_text='Embeds attached to this message.', size=None),
+ field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.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/migrations/0051_delete_tag.py b/pydis_site/apps/api/migrations/0051_delete_tag.py
new file mode 100644
index 00000000..bada5788
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0051_delete_tag.py
@@ -0,0 +1,16 @@
+# Generated by Django 2.2.11 on 2020-04-01 06:15
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0050_remove_infractions_active_default_value'),
+ ]
+
+ operations = [
+ migrations.DeleteModel(
+ name='Tag',
+ ),
+ ]
diff --git a/pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py
new file mode 100644
index 00000000..dfdf3835
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.11 on 2020-03-30 10:24
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0051_create_news_setting'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ 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/migrations/0061_merge_20200830_0526.py b/pydis_site/apps/api/migrations/0061_merge_20200830_0526.py
new file mode 100644
index 00000000..f0668696
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0061_merge_20200830_0526.py
@@ -0,0 +1,14 @@
+# Generated by Django 3.0.8 on 2020-08-30 05:26
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0060_populate_filterlists_fix'),
+ ('api', '0052_offtopicchannelname_used'),
+ ]
+
+ operations = [
+ ]
diff --git a/pydis_site/apps/api/migrations/0062_merge_20200901_1459.py b/pydis_site/apps/api/migrations/0062_merge_20200901_1459.py
new file mode 100644
index 00000000..d162acf1
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0062_merge_20200901_1459.py
@@ -0,0 +1,14 @@
+# Generated by Django 3.0.8 on 2020-09-01 14:59
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0051_delete_tag'),
+ ('api', '0061_merge_20200830_0526'),
+ ]
+
+ operations = [
+ ]
diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py
index 1d0ab7ea..e3f928e1 100644
--- a/pydis_site/apps/api/models/__init__.py
+++ b/pydis_site/apps/api/models/__init__.py
@@ -12,7 +12,6 @@ from .bot import (
OffTopicChannelName,
Reminder,
Role,
- Tag,
User
)
from .log_entry import LogEntry
diff --git a/pydis_site/apps/api/models/bot/__init__.py b/pydis_site/apps/api/models/bot/__init__.py
index efd98184..1673b434 100644
--- a/pydis_site/apps/api/models/bot/__init__.py
+++ b/pydis_site/apps/api/models/bot/__init__.py
@@ -11,5 +11,4 @@ from .off_topic_channel_name import OffTopicChannelName
from .offensive_message import OffensiveMessage
from .reminder import Reminder
from .role import Role
-from .tag import Tag
from .user import User
diff --git a/pydis_site/apps/api/models/bot/message.py b/pydis_site/apps/api/models/bot/message.py
index 78dcbf1d..f6ae55a5 100644
--- a/pydis_site/apps/api/models/bot/message.py
+++ b/pydis_site/apps/api/models/bot/message.py
@@ -5,9 +5,9 @@ from django.core.validators import MinValueValidator
from django.db import models
from django.utils import timezone
-from pydis_site.apps.api.models.bot.tag import validate_tag_embed
from pydis_site.apps.api.models.bot.user import User
from pydis_site.apps.api.models.mixins import ModelReprMixin
+from pydis_site.apps.api.models.utils import validate_embed
class Message(ModelReprMixin, models.Model):
@@ -47,7 +47,7 @@ class Message(ModelReprMixin, models.Model):
)
embeds = pgfields.ArrayField(
pgfields.JSONField(
- validators=(validate_tag_embed,)
+ validators=(validate_embed,)
),
blank=True,
help_text="Embeds attached to this message."
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 20e77b9f..403c7465 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
@@ -16,6 +16,11 @@ class OffTopicChannelName(ModelReprMixin, models.Model):
help_text="The actual channel name that will be used on our Discord server."
)
+ used = models.BooleanField(
+ default=False,
+ help_text="Whether or not this name has already been used during this rotation",
+ )
+
def __str__(self):
"""Returns the current off-topic name, for display purposes."""
return self.name
diff --git a/pydis_site/apps/api/models/bot/tag.py b/pydis_site/apps/api/models/utils.py
index 5e53582f..107231ba 100644
--- a/pydis_site/apps/api/models/bot/tag.py
+++ b/pydis_site/apps/api/models/utils.py
@@ -1,12 +1,8 @@
from collections.abc import Mapping
from typing import Any, Dict
-from django.contrib.postgres import fields as pgfields
from django.core.exceptions import ValidationError
from django.core.validators import MaxLengthValidator, MinLengthValidator
-from django.db import models
-
-from pydis_site.apps.api.models.mixins import ModelReprMixin
def is_bool_validator(value: Any) -> None:
@@ -15,7 +11,7 @@ def is_bool_validator(value: Any) -> None:
raise ValidationError(f"This field must be of type bool, not {type(value)}.")
-def validate_tag_embed_fields(fields: dict) -> None:
+def validate_embed_fields(fields: dict) -> None:
"""Raises a ValidationError if any of the given embed fields is invalid."""
field_validators = {
'name': (MaxLengthValidator(limit_value=256),),
@@ -42,7 +38,7 @@ def validate_tag_embed_fields(fields: dict) -> None:
validator(value)
-def validate_tag_embed_footer(footer: Dict[str, str]) -> None:
+def validate_embed_footer(footer: Dict[str, str]) -> None:
"""Raises a ValidationError if the given footer is invalid."""
field_validators = {
'text': (
@@ -67,7 +63,7 @@ def validate_tag_embed_footer(footer: Dict[str, str]) -> None:
validator(value)
-def validate_tag_embed_author(author: Any) -> None:
+def validate_embed_author(author: Any) -> None:
"""Raises a ValidationError if the given author is invalid."""
field_validators = {
'name': (
@@ -93,7 +89,7 @@ def validate_tag_embed_author(author: Any) -> None:
validator(value)
-def validate_tag_embed(embed: Any) -> None:
+def validate_embed(embed: Any) -> None:
"""
Validate a JSON document containing an embed as possible to send on Discord.
@@ -109,11 +105,11 @@ def validate_tag_embed(embed: Any) -> None:
>>> from django.contrib.postgres import fields as pgfields
>>> from django.db import models
- >>> from pydis_site.apps.api.models.bot.tag import validate_tag_embed
+ >>> from pydis_site.apps.api.models.utils import validate_embed
>>> class MyMessage(models.Model):
... embed = pgfields.JSONField(
... validators=(
- ... validate_tag_embed,
+ ... validate_embed,
... )
... )
... # ...
@@ -149,10 +145,10 @@ def validate_tag_embed(embed: Any) -> None:
'description': (MaxLengthValidator(limit_value=2048),),
'fields': (
MaxLengthValidator(limit_value=25),
- validate_tag_embed_fields
+ validate_embed_fields
),
- 'footer': (validate_tag_embed_footer,),
- 'author': (validate_tag_embed_author,)
+ 'footer': (validate_embed_footer,),
+ 'author': (validate_embed_author,)
}
if not embed:
@@ -175,24 +171,3 @@ def validate_tag_embed(embed: Any) -> None:
if field_name in field_validators:
for validator in field_validators[field_name]:
validator(value)
-
-
-class Tag(ModelReprMixin, models.Model):
- """A tag providing (hopefully) useful information."""
-
- title = models.CharField(
- max_length=100,
- help_text=(
- "The title of this tag, shown in searches and providing "
- "a quick overview over what this embed contains."
- ),
- primary_key=True
- )
- embed = pgfields.JSONField(
- help_text="The actual embed shown by this tag.",
- validators=(validate_tag_embed,)
- )
-
- def __str__(self):
- """Returns the title of this tag, for display purposes."""
- return self.title
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index 0915658a..ae57b307 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -22,7 +22,6 @@ from .models import (
OffensiveMessage,
Reminder,
Role,
- Tag,
User
)
@@ -256,16 +255,6 @@ class RoleSerializer(ModelSerializer):
fields = ('id', 'name', 'colour', 'permissions', 'position')
-class TagSerializer(ModelSerializer):
- """A class providing (de-)serialization of `Tag` instances."""
-
- class Meta:
- """Metadata defined for the Django REST Framework."""
-
- model = Tag
- fields = ('title', 'embed')
-
-
class UserListSerializer(ListSerializer):
"""List serializer for User model to handle bulk updates."""
diff --git a/pydis_site/apps/api/tests/test_models.py b/pydis_site/apps/api/tests/test_models.py
index e0e347bb..853e6621 100644
--- a/pydis_site/apps/api/tests/test_models.py
+++ b/pydis_site/apps/api/tests/test_models.py
@@ -14,7 +14,6 @@ from pydis_site.apps.api.models import (
OffensiveMessage,
Reminder,
Role,
- Tag,
User
)
from pydis_site.apps.api.models.mixins import ModelReprMixin
@@ -104,10 +103,6 @@ class StringDunderMethodTests(SimpleTestCase):
),
creation=dt.utcnow()
),
- Tag(
- title='bob',
- embed={'content': "the builder"}
- ),
User(
id=5,
name='bob',
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 bd42cd81..3ab8b22d 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
@@ -10,12 +10,14 @@ class UnauthenticatedTests(APISubdomainTestCase):
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')
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')
response = self.client.get(f'{url}?random_items=no')
@@ -24,6 +26,7 @@ class UnauthenticatedTests(APISubdomainTestCase):
class EmptyDatabaseTests(APISubdomainTestCase):
def test_returns_empty_object(self):
+ """Return empty list when no names in database."""
url = reverse('bot:offtopicchannelname-list', host='api')
response = self.client.get(url)
@@ -31,6 +34,7 @@ class EmptyDatabaseTests(APISubdomainTestCase):
self.assertEqual(response.json(), [])
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')
response = self.client.get(f'{url}?random_items=5')
@@ -38,6 +42,7 @@ class EmptyDatabaseTests(APISubdomainTestCase):
self.assertEqual(response.json(), [])
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')
response = self.client.get(f'{url}?random_items=totally-a-valid-integer')
@@ -47,6 +52,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')
response = self.client.get(f'{url}?random_items=-5')
@@ -59,10 +65,11 @@ class EmptyDatabaseTests(APISubdomainTestCase):
class ListTests(APISubdomainTestCase):
@classmethod
def setUpTestData(cls):
- cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand')
- cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk')
+ 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)
def test_returns_name_in_list(self):
+ """Return all off-topic channel names."""
url = reverse('bot:offtopicchannelname-list', host='api')
response = self.client.get(url)
@@ -76,11 +83,21 @@ 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')
response = self.client.get(f'{url}?random_items=1')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 1)
+ self.assertEqual(response.json(), [self.test_name.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')
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json(), [self.test_name.name, self.test_name_2.name])
class CreationTests(APISubdomainTestCase):
@@ -93,6 +110,7 @@ class CreationTests(APISubdomainTestCase):
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')
names = (
'𝖠𝖡𝖢𝖣𝖤𝖥𝖦𝖧𝖨𝖩𝖪𝖫𝖬𝖭𝖮𝖯𝖰𝖱𝖲𝖳𝖴𝖵𝖶𝖷𝖸𝖹',
@@ -104,6 +122,7 @@ class CreationTests(APISubdomainTestCase):
self.assertEqual(response.status_code, 201)
def test_returns_400_for_missing_name_param(self):
+ """Return error message when name not provided."""
url = reverse('bot:offtopicchannelname-list', host='api')
response = self.client.post(url)
self.assertEqual(response.status_code, 400)
@@ -112,6 +131,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')
invalid_names = (
'space between words',
@@ -134,18 +154,21 @@ class DeletionTests(APISubdomainTestCase):
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."""
url = reverse('bot:offtopicchannelname-detail', args=('unknown-name',), host='api')
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')
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')
response = self.client.delete(url)
diff --git a/pydis_site/apps/api/tests/test_validators.py b/pydis_site/apps/api/tests/test_validators.py
index 241af08c..8bb7b917 100644
--- a/pydis_site/apps/api/tests/test_validators.py
+++ b/pydis_site/apps/api/tests/test_validators.py
@@ -5,7 +5,7 @@ from django.test import TestCase
from ..models.bot.bot_setting import validate_bot_setting_name
from ..models.bot.offensive_message import future_date_validator
-from ..models.bot.tag import validate_tag_embed
+from ..models.utils import validate_embed
REQUIRED_KEYS = (
@@ -25,77 +25,77 @@ class BotSettingValidatorTests(TestCase):
class TagEmbedValidatorTests(TestCase):
def test_rejects_non_mapping(self):
with self.assertRaises(ValidationError):
- validate_tag_embed('non-empty non-mapping')
+ validate_embed('non-empty non-mapping')
def test_rejects_missing_required_keys(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'unknown': "key"
})
def test_rejects_one_correct_one_incorrect(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'provider': "??",
'title': ""
})
def test_rejects_empty_required_key(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'title': ''
})
def test_rejects_list_as_embed(self):
with self.assertRaises(ValidationError):
- validate_tag_embed([])
+ validate_embed([])
def test_rejects_required_keys_and_unknown_keys(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'title': "the duck walked up to the lemonade stand",
'and': "he said to the man running the stand"
})
def test_rejects_too_long_title(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'title': 'a' * 257
})
def test_rejects_too_many_fields(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'fields': [{} for _ in range(26)]
})
def test_rejects_too_long_description(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'description': 'd' * 2049
})
def test_allows_valid_embed(self):
- validate_tag_embed({
+ validate_embed({
'title': "My embed",
'description': "look at my embed, my embed is amazing"
})
def test_allows_unvalidated_fields(self):
- validate_tag_embed({
+ validate_embed({
'title': "My embed",
'provider': "what am I??"
})
def test_rejects_fields_as_list_of_non_mappings(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'fields': ['abc']
})
def test_rejects_fields_with_unknown_fields(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'fields': [
{
'what': "is this field"
@@ -105,7 +105,7 @@ class TagEmbedValidatorTests(TestCase):
def test_rejects_fields_with_too_long_name(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'fields': [
{
'name': "a" * 257
@@ -115,7 +115,7 @@ class TagEmbedValidatorTests(TestCase):
def test_rejects_one_correct_one_incorrect_field(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'fields': [
{
'name': "Totally valid",
@@ -131,7 +131,7 @@ class TagEmbedValidatorTests(TestCase):
def test_rejects_missing_required_field_field(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'fields': [
{
'name': "Totally valid",
@@ -142,7 +142,7 @@ class TagEmbedValidatorTests(TestCase):
def test_rejects_invalid_inline_field_field(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'fields': [
{
'name': "Totally valid",
@@ -153,7 +153,7 @@ class TagEmbedValidatorTests(TestCase):
})
def test_allows_valid_fields(self):
- validate_tag_embed({
+ validate_embed({
'fields': [
{
'name': "valid",
@@ -174,14 +174,14 @@ class TagEmbedValidatorTests(TestCase):
def test_rejects_footer_as_non_mapping(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'title': "whatever",
'footer': []
})
def test_rejects_footer_with_unknown_fields(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'title': "whatever",
'footer': {
'duck': "quack"
@@ -190,7 +190,7 @@ class TagEmbedValidatorTests(TestCase):
def test_rejects_footer_with_empty_text(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'title': "whatever",
'footer': {
'text': ""
@@ -198,7 +198,7 @@ class TagEmbedValidatorTests(TestCase):
})
def test_allows_footer_with_proper_values(self):
- validate_tag_embed({
+ validate_embed({
'title': "whatever",
'footer': {
'text': "django good"
@@ -207,14 +207,14 @@ class TagEmbedValidatorTests(TestCase):
def test_rejects_author_as_non_mapping(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'title': "whatever",
'author': []
})
def test_rejects_author_with_unknown_field(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'title': "whatever",
'author': {
'field': "that is unknown"
@@ -223,7 +223,7 @@ class TagEmbedValidatorTests(TestCase):
def test_rejects_author_with_empty_name(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'title': "whatever",
'author': {
'name': ""
@@ -232,7 +232,7 @@ class TagEmbedValidatorTests(TestCase):
def test_rejects_author_with_one_correct_one_incorrect(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'title': "whatever",
'author': {
# Relies on "dictionary insertion order remembering" (D.I.O.R.) behaviour
@@ -242,7 +242,7 @@ class TagEmbedValidatorTests(TestCase):
})
def test_allows_author_with_proper_values(self):
- validate_tag_embed({
+ validate_embed({
'title': "whatever",
'author': {
'name': "Bob"
diff --git a/pydis_site/apps/api/urls.py b/pydis_site/apps/api/urls.py
index a4fd5b2e..4dbf93db 100644
--- a/pydis_site/apps/api/urls.py
+++ b/pydis_site/apps/api/urls.py
@@ -14,7 +14,6 @@ from .viewsets import (
OffensiveMessageViewSet,
ReminderViewSet,
RoleViewSet,
- TagViewSet,
UserViewSet
)
@@ -62,10 +61,6 @@ bot_router.register(
RoleViewSet
)
bot_router.register(
- 'tags',
- TagViewSet
-)
-bot_router.register(
'users',
UserViewSet
)
diff --git a/pydis_site/apps/api/viewsets/__init__.py b/pydis_site/apps/api/viewsets/__init__.py
index 8699517e..dfbb880d 100644
--- a/pydis_site/apps/api/viewsets/__init__.py
+++ b/pydis_site/apps/api/viewsets/__init__.py
@@ -10,7 +10,6 @@ from .bot import (
OffTopicChannelNameViewSet,
ReminderViewSet,
RoleViewSet,
- TagViewSet,
UserViewSet
)
from .log_entry import LogEntryViewSet
diff --git a/pydis_site/apps/api/viewsets/bot/__init__.py b/pydis_site/apps/api/viewsets/bot/__init__.py
index e64e3988..84b87eab 100644
--- a/pydis_site/apps/api/viewsets/bot/__init__.py
+++ b/pydis_site/apps/api/viewsets/bot/__init__.py
@@ -9,5 +9,4 @@ from .off_topic_channel_name import OffTopicChannelNameViewSet
from .offensive_message import OffensiveMessageViewSet
from .reminder import ReminderViewSet
from .role import RoleViewSet
-from .tag import TagViewSet
from .user import UserViewSet
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 d6da2399..826ad25e 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,3 +1,4 @@
+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
@@ -20,7 +21,9 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet):
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
- ... then the API will return `5` random items from the database.
+ ... 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.
#### Response format
Return a list of off-topic-channel names:
@@ -106,7 +109,27 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet):
'random_items': ["Must be a positive integer."]
})
- queryset = self.get_queryset().order_by('?')[:random_count]
+ queryset = self.get_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(
+ used=Case(
+ When(
+ name__in=(offtopic_name.name for offtopic_name in queryset),
+ then=Value(True)
+ ),
+ default=Value(False)
+ )
+ )
+ else:
+ # Otherwise mark selected names `used` to True
+ self.get_queryset().filter(
+ name__in=(offtopic_name.name for offtopic_name in queryset)
+ ).update(used=True)
+
serialized = self.serializer_class(queryset, many=True)
return Response(serialized.data)
diff --git a/pydis_site/apps/api/viewsets/bot/tag.py b/pydis_site/apps/api/viewsets/bot/tag.py
deleted file mode 100644
index 7e9ba117..00000000
--- a/pydis_site/apps/api/viewsets/bot/tag.py
+++ /dev/null
@@ -1,105 +0,0 @@
-from rest_framework.viewsets import ModelViewSet
-
-from pydis_site.apps.api.models.bot.tag import Tag
-from pydis_site.apps.api.serializers import TagSerializer
-
-
-class TagViewSet(ModelViewSet):
- """
- View providing CRUD operations on tags shown by our bot.
-
- ## Routes
- ### GET /bot/tags
- Returns all tags in the database.
-
- #### Response format
- >>> [
- ... {
- ... 'title': "resources",
- ... 'embed': {
- ... 'content': "Did you really think I'd put something useful here?"
- ... }
- ... }
- ... ]
-
- #### Status codes
- - 200: returned on success
-
- ### GET /bot/tags/<title:str>
- Gets a single tag by its title.
-
- #### Response format
- >>> {
- ... 'title': "My awesome tag",
- ... 'embed': {
- ... 'content': "totally not filler words"
- ... }
- ... }
-
- #### Status codes
- - 200: returned on success
- - 404: if a tag with the given `title` could not be found
-
- ### POST /bot/tags
- Adds a single tag to the database.
-
- #### Request body
- >>> {
- ... 'title': str,
- ... 'embed': dict
- ... }
-
- The embed structure is the same as the embed structure that the Discord API
- expects. You can view the documentation for it here:
- https://discordapp.com/developers/docs/resources/channel#embed-object
-
- #### Status codes
- - 201: returned on success
- - 400: if one of the given fields is invalid
-
- ### PUT /bot/tags/<title:str>
- Update the tag with the given `title`.
-
- #### Request body
- >>> {
- ... 'title': str,
- ... 'embed': dict
- ... }
-
- The embed structure is the same as the embed structure that the Discord API
- expects. You can view the documentation for it here:
- https://discordapp.com/developers/docs/resources/channel#embed-object
-
- #### Status codes
- - 200: returned on success
- - 400: if the request body was invalid, see response body for details
- - 404: if the tag with the given `title` could not be found
-
- ### PATCH /bot/tags/<title:str>
- Update the tag with the given `title`.
-
- #### Request body
- >>> {
- ... 'title': str,
- ... 'embed': dict
- ... }
-
- The embed structure is the same as the embed structure that the Discord API
- expects. You can view the documentation for it here:
- https://discordapp.com/developers/docs/resources/channel#embed-object
-
- #### Status codes
- - 200: returned on success
- - 400: if the request body was invalid, see response body for details
- - 404: if the tag with the given `title` could not be found
-
- ### DELETE /bot/tags/<title:str>
- Deletes the tag with the given `title`.
-
- #### Status codes
- - 204: returned on success
- - 404: if a tag with the given `title` does not exist
- """
-
- serializer_class = TagSerializer
- queryset = Tag.objects.all()
diff --git a/pydis_site/apps/home/tests/mock_github_api_response.json b/pydis_site/apps/home/tests/mock_github_api_response.json
index 35604a85..10be4f99 100644
--- a/pydis_site/apps/home/tests/mock_github_api_response.json
+++ b/pydis_site/apps/home/tests/mock_github_api_response.json
@@ -28,7 +28,7 @@
"forks_count": 31
},
{
- "full_name": "python-discord/flake8-annotations",
+ "full_name": "python-discord/metricity",
"description": "test",
"stargazers_count": 97,
"language": "Python",
diff --git a/pydis_site/apps/home/views/home.py b/pydis_site/apps/home/views/home.py
index 20e38ab0..3b5cd5ac 100644
--- a/pydis_site/apps/home/views/home.py
+++ b/pydis_site/apps/home/views/home.py
@@ -23,7 +23,7 @@ class HomeView(View):
"python-discord/bot",
"python-discord/snekbox",
"python-discord/seasonalbot",
- "python-discord/flake8-annotations",
+ "python-discord/metricity",
"python-discord/django-simple-bulma",
]
diff --git a/pydis_site/constants.py b/pydis_site/constants.py
new file mode 100644
index 00000000..0b76694a
--- /dev/null
+++ b/pydis_site/constants.py
@@ -0,0 +1,5 @@
+import git
+
+# Git SHA
+repo = git.Repo(search_parent_directories=True)
+GIT_SHA = repo.head.object.hexsha
diff --git a/pydis_site/context_processors.py b/pydis_site/context_processors.py
new file mode 100644
index 00000000..6937a3db
--- /dev/null
+++ b/pydis_site/context_processors.py
@@ -0,0 +1,8 @@
+from django.template import RequestContext
+
+from pydis_site.constants import GIT_SHA
+
+
+def git_sha_processor(_: RequestContext) -> dict:
+ """Expose the git SHA for this repo to all views."""
+ return {'git_sha': GIT_SHA}
diff --git a/pydis_site/settings.py b/pydis_site/settings.py
index 2c87007c..1f042c1b 100644
--- a/pydis_site/settings.py
+++ b/pydis_site/settings.py
@@ -20,6 +20,7 @@ import sentry_sdk
from django.contrib.messages import constants as messages
from sentry_sdk.integrations.django import DjangoIntegration
+from pydis_site.constants import GIT_SHA
if typing.TYPE_CHECKING:
from django.contrib.auth.models import User
@@ -33,7 +34,8 @@ env = environ.Env(
sentry_sdk.init(
dsn=env('SITE_SENTRY_DSN'),
integrations=[DjangoIntegration()],
- send_default_pii=True
+ send_default_pii=True,
+ release=f"pydis-site@{GIT_SHA}"
)
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
@@ -157,8 +159,8 @@ TEMPLATES = [
'django.template.context_processors.static',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
-
"sekizai.context_processors.sekizai",
+ "pydis_site.context_processors.git_sha_processor"
],
},
},
diff --git a/pydis_site/templates/base/base.html b/pydis_site/templates/base/base.html
index 4c70d778..70426dc1 100644
--- a/pydis_site/templates/base/base.html
+++ b/pydis_site/templates/base/base.html
@@ -37,6 +37,7 @@
{% render_block "css" %}
</head>
<body class="site">
+ <!-- Git hash for this release: {{ git_sha }} -->
<main class="site-content">
{% if messages %}