From e2956b87289a37747cb5431ad3a08dc202a2bcba Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sun, 20 Oct 2019 05:37:15 +1000 Subject: Set newest-first sorting for message deletion models, add log_url property. --- pydis_site/apps/api/models/bot/deleted_message.py | 4 ++-- pydis_site/apps/api/models/bot/message_deletion_context.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/bot/deleted_message.py b/pydis_site/apps/api/models/bot/deleted_message.py index 1eb4516e..50b70d8c 100644 --- a/pydis_site/apps/api/models/bot/deleted_message.py +++ b/pydis_site/apps/api/models/bot/deleted_message.py @@ -14,6 +14,6 @@ class DeletedMessage(Message): ) class Meta: - """Sets the default ordering for list views to oldest first.""" + """Sets the default ordering for list views to newest first.""" - ordering = ["id"] + ordering = ("-id",) diff --git a/pydis_site/apps/api/models/bot/message_deletion_context.py b/pydis_site/apps/api/models/bot/message_deletion_context.py index 44a0c8ae..02a15ca0 100644 --- a/pydis_site/apps/api/models/bot/message_deletion_context.py +++ b/pydis_site/apps/api/models/bot/message_deletion_context.py @@ -1,3 +1,4 @@ +from django.contrib.sites.models import Site from django.db import models from pydis_site.apps.api.models.bot.user import User @@ -28,3 +29,14 @@ class MessageDeletionContext(ModelReprMixin, models.Model): # the deletion context does not take place in the future. help_text="When this deletion took place." ) + + @property + def log_url(self) -> str: + """Create the url for the deleted message logs.""" + domain = Site.objects.get_current().domain + return f"http://staff.{domain}/bot/logs/{self.id}/" + + class Meta: + """Set the ordering for list views to newest first.""" + + ordering = ("-creation",) -- cgit v1.2.3 From 618610fe367b0c8d6175d251c276c2e37db8aa52 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sun, 20 Oct 2019 05:43:21 +1000 Subject: Order roles by positioning, add filters and search to api user admin page. --- pydis_site/apps/api/admin.py | 45 ++++++++++++++++++++++++++++++++-- pydis_site/apps/api/models/bot/role.py | 5 ++++ 2 files changed, 48 insertions(+), 2 deletions(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index 6d6a9b3b..65cc0a6c 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -1,8 +1,9 @@ import json -from typing import Optional +from typing import Optional, Tuple from django import urls from django.contrib import admin +from django.db.models.query import QuerySet from django.http import HttpRequest from django.utils.html import format_html @@ -121,6 +122,46 @@ class MessageDeletionContextAdmin(admin.ModelAdmin): ) +class StaffRolesFilter(admin.SimpleListFilter): + """Filter options for Staff Roles.""" + + title = "Staff Role" + parameter_name = "staff_role" + + @staticmethod + def lookups(*_) -> Tuple[Tuple[str, str], ...]: + """Available filter options.""" + return ( + ("Owners", "Owners"), + ("Admins", "Admins"), + ("Moderators", "Moderators"), + ("Core Developers", "Core Developers"), + ("Helpers", "Helpers"), + ) + + def queryset(self, request: HttpRequest, queryset: QuerySet) -> Optional[QuerySet]: + """Returned data filter based on selected option.""" + value = self.value() + if value: + return queryset.filter(roles__name=value) + + +class UserAdmin(admin.ModelAdmin): + """Admin formatting for the User model.""" + + search_fields = ("name", "id", "roles__name", "roles__id") + list_filter = ("in_guild", StaffRolesFilter) + exclude = ("name", "discriminator") + readonly_fields = ( + "__str__", + "id", + "avatar_hash", + "top_role", + "roles", + "in_guild", + ) + + admin.site.register(BotSetting) admin.site.register(DeletedMessage, DeletedMessageAdmin) admin.site.register(DocumentationLink) @@ -131,4 +172,4 @@ admin.site.register(Nomination) admin.site.register(OffTopicChannelName) admin.site.register(Role) admin.site.register(Tag) -admin.site.register(User) +admin.site.register(User, UserAdmin) diff --git a/pydis_site/apps/api/models/bot/role.py b/pydis_site/apps/api/models/bot/role.py index 58bbf8b4..b95740da 100644 --- a/pydis_site/apps/api/models/bot/role.py +++ b/pydis_site/apps/api/models/bot/role.py @@ -65,3 +65,8 @@ class Role(ModelReprMixin, models.Model): def __le__(self, other: Role) -> bool: """Compares the roles based on their position in the role hierarchy of the guild.""" return self.position <= other.position + + class Meta: + """Set role ordering from highest to lowest position.""" + + ordering = ("-position",) -- cgit v1.2.3 From 1c41c8a1aa07d7a716561c7594f769db7aa58cf5 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sun, 20 Oct 2019 06:33:35 +1000 Subject: Improve nominations admin list and page, add search and filter by active. --- pydis_site/apps/api/admin.py | 39 +++++++++++++++++++++++++++- pydis_site/apps/api/models/bot/nomination.py | 5 ++++ 2 files changed, 43 insertions(+), 1 deletion(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index 74b9413b..55a9d655 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -166,6 +166,43 @@ class InfractionAdmin(admin.ModelAdmin): ) +class NominationAdmin(admin.ModelAdmin): + """Admin formatting for the Nomination model.""" + + list_display = ( + "user", + "active", + "reason", + "actor", + "inserted_at", + "ended_at" + ) + fields = ( + "user", + "active", + "actor", + "reason", + "inserted_at", + "ended_at", + "end_reason" + ) + readonly_fields = ( + "user", + "active", + "actor", + "inserted_at", + "ended_at" + ) + search_fields = ( + "actor__name", + "actor__id", + "user__name", + "user__id", + "reason" + ) + list_filter = ("active",) + + class StaffRolesFilter(admin.SimpleListFilter): """Filter options for Staff Roles.""" @@ -212,7 +249,7 @@ admin.site.register(DocumentationLink) admin.site.register(Infraction, InfractionAdmin) admin.site.register(LogEntry, LogEntryAdmin) admin.site.register(MessageDeletionContext, MessageDeletionContextAdmin) -admin.site.register(Nomination) +admin.site.register(Nomination, NominationAdmin) admin.site.register(OffTopicChannelName) admin.site.register(Role) admin.site.register(Tag) diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py index cd9951aa..a0ba42a3 100644 --- a/pydis_site/apps/api/models/bot/nomination.py +++ b/pydis_site/apps/api/models/bot/nomination.py @@ -44,3 +44,8 @@ class Nomination(ModelReprMixin, models.Model): """Representation that makes the target and state of the nomination immediately evident.""" status = "active" if self.active else "ended" return f"Nomination of {self.user} ({status})" + + class Meta: + """Set the ordering of nominations to most recent first.""" + + ordering = ("-inserted_at",) -- cgit v1.2.3 From 56c6c85ad973a3b594d72215cdfb2d3b6c19d741 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sun, 20 Oct 2019 09:29:58 +1000 Subject: Add new test for deleted message context log_url. --- .../api/models/bot/message_deletion_context.py | 5 ++--- pydis_site/apps/api/tests/test_deleted_messages.py | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/bot/message_deletion_context.py b/pydis_site/apps/api/models/bot/message_deletion_context.py index 02a15ca0..fde9b0a6 100644 --- a/pydis_site/apps/api/models/bot/message_deletion_context.py +++ b/pydis_site/apps/api/models/bot/message_deletion_context.py @@ -1,5 +1,5 @@ -from django.contrib.sites.models import Site from django.db import models +from django_hosts.resolvers import reverse from pydis_site.apps.api.models.bot.user import User from pydis_site.apps.api.models.utils import ModelReprMixin @@ -33,8 +33,7 @@ class MessageDeletionContext(ModelReprMixin, models.Model): @property def log_url(self) -> str: """Create the url for the deleted message logs.""" - domain = Site.objects.get_current().domain - return f"http://staff.{domain}/bot/logs/{self.id}/" + return reverse('logs', host="staff", args=(self.id,)) class Meta: """Set the ordering for list views to newest first.""" diff --git a/pydis_site/apps/api/tests/test_deleted_messages.py b/pydis_site/apps/api/tests/test_deleted_messages.py index d1e9f2f5..ccccdda4 100644 --- a/pydis_site/apps/api/tests/test_deleted_messages.py +++ b/pydis_site/apps/api/tests/test_deleted_messages.py @@ -1,5 +1,6 @@ from datetime import datetime +from django.utils import timezone from django_hosts.resolvers import reverse from .base import APISubdomainTestCase @@ -75,3 +76,24 @@ class DeletedMessagesWithActorTests(APISubdomainTestCase): self.assertEqual(response.status_code, 201) [context] = MessageDeletionContext.objects.all() self.assertEqual(context.actor.id, self.actor.id) + + +class DeletedMessagesLogURLTests(APISubdomainTestCase): + @classmethod + def setUpTestData(cls): # noqa + cls.author = cls.actor = User.objects.create( + id=324888, + name='Black Knight', + discriminator=1975, + avatar_hash=None + ) + + cls.deletion_context = MessageDeletionContext.objects.create( + actor=cls.actor, + creation=timezone.now() + ) + + def test_valid_log_url(self): + expected_url = reverse('logs', host="staff", args=(1,)) + [context] = MessageDeletionContext.objects.all() + self.assertEqual(context.log_url, expected_url) -- cgit v1.2.3 From 46ef497fd028531ffac8ebc19dec41a3272f72a3 Mon Sep 17 00:00:00 2001 From: ks123 Date: Mon, 30 Mar 2020 13:30:00 +0300 Subject: (Off-topic Channel Names): Added new field to model: `used` that show is this name already used on this round of names, added migration for this. --- .../api/migrations/0051_offtopicchannelname_used.py | 18 ++++++++++++++++++ .../apps/api/models/bot/off_topic_channel_name.py | 5 +++++ 2 files changed, 23 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py new file mode 100644 index 00000000..74836d8c --- /dev/null +++ b/pydis_site/apps/api/migrations/0051_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', '0050_remove_infractions_active_default_value'), + ] + + operations = [ + migrations.AddField( + model_name='offtopicchannelname', + name='used', + field=models.BooleanField(default=False, help_text='Show is channel already used as channel name in this round.'), + ), + ] 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 29280c27..3345754d 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="Show is channel already used as channel name in this round." + ) + def __str__(self): """Returns the current off-topic name, for display purposes.""" return self.name -- cgit v1.2.3 From e321a25c05875e82073470429708b37849947b16 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 28 May 2020 08:23:14 +0300 Subject: OT: Replace help text of `used` field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Leon Sandøy --- pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py | 2 +- pydis_site/apps/api/models/bot/off_topic_channel_name.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py index 74836d8c..1b838aec 100644 --- a/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py +++ b/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py @@ -13,6 +13,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='offtopicchannelname', name='used', - field=models.BooleanField(default=False, help_text='Show is channel already used as channel name in this round.'), + 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/models/bot/off_topic_channel_name.py b/pydis_site/apps/api/models/bot/off_topic_channel_name.py index 3345754d..413cbfae 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 @@ -18,7 +18,7 @@ class OffTopicChannelName(ModelReprMixin, models.Model): used = models.BooleanField( default=False, - help_text="Show is channel already used as channel name in this round." + help_text="Whether or not this name has already been used during this rotation", ) def __str__(self): -- cgit v1.2.3 From 790c50cbc2d45134fc0b668bc21ca7e01e4af2a2 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 08:46:19 +0300 Subject: (Tag Cleanup): Removed Tags Model import from models __init__.py --- pydis_site/apps/api/models/bot/__init__.py | 1 - 1 file changed, 1 deletion(-) (limited to 'pydis_site/apps/api/models') 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 -- cgit v1.2.3 From b16809620f444872f4d295ee55e4e8e9cdde12c8 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 08:49:49 +0300 Subject: (Tag Cleanup): Moved embed validators from Tag model to utils.py --- pydis_site/apps/api/models/mixins.py | 173 +++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py index 5d75b78b..2d658cb9 100644 --- a/pydis_site/apps/api/models/mixins.py +++ b/pydis_site/apps/api/models/mixins.py @@ -1,4 +1,177 @@ +from collections.abc import Mapping from operator import itemgetter +from typing import Any, Dict + +from django.core.exceptions import ValidationError +from django.core.validators import MaxLengthValidator, MinLengthValidator + + +def is_bool_validator(value: Any) -> None: + """Validates if a given value is of type bool.""" + if not isinstance(value, bool): + raise ValidationError(f"This field must be of type bool, not {type(value)}.") + + +def validate_tag_embed_fields(fields: dict) -> None: + """Raises a ValidationError if any of the given embed fields is invalid.""" + field_validators = { + 'name': (MaxLengthValidator(limit_value=256),), + 'value': (MaxLengthValidator(limit_value=1024),), + 'inline': (is_bool_validator,), + } + + required_fields = ('name', 'value') + + for field in fields: + if not isinstance(field, Mapping): + raise ValidationError("Embed fields must be a mapping.") + + if not all(required_field in field for required_field in required_fields): + raise ValidationError( + f"Embed fields must contain the following fields: {', '.join(required_fields)}." + ) + + for field_name, value in field.items(): + if field_name not in field_validators: + raise ValidationError(f"Unknown embed field field: {field_name!r}.") + + for validator in field_validators[field_name]: + validator(value) + + +def validate_tag_embed_footer(footer: Dict[str, str]) -> None: + """Raises a ValidationError if the given footer is invalid.""" + field_validators = { + 'text': ( + MinLengthValidator( + limit_value=1, + message="Footer text must not be empty." + ), + MaxLengthValidator(limit_value=2048) + ), + 'icon_url': (), + 'proxy_icon_url': () + } + + if not isinstance(footer, Mapping): + raise ValidationError("Embed footer must be a mapping.") + + for field_name, value in footer.items(): + if field_name not in field_validators: + raise ValidationError(f"Unknown embed footer field: {field_name!r}.") + + for validator in field_validators[field_name]: + validator(value) + + +def validate_tag_embed_author(author: Any) -> None: + """Raises a ValidationError if the given author is invalid.""" + field_validators = { + 'name': ( + MinLengthValidator( + limit_value=1, + message="Embed author name must not be empty." + ), + MaxLengthValidator(limit_value=256) + ), + 'url': (), + 'icon_url': (), + 'proxy_icon_url': () + } + + if not isinstance(author, Mapping): + raise ValidationError("Embed author must be a mapping.") + + for field_name, value in author.items(): + if field_name not in field_validators: + raise ValidationError(f"Unknown embed author field: {field_name!r}.") + + for validator in field_validators[field_name]: + validator(value) + + +def validate_tag_embed(embed: Any) -> None: + """ + Validate a JSON document containing an embed as possible to send on Discord. + + This attempts to rebuild the validation used by Discord + as well as possible by checking for various embed limits so we can + ensure that any embed we store here will also be accepted as a + valid embed by the Discord API. + + Using this directly is possible, although not intended - you usually + stick this onto the `validators` keyword argument of model fields. + + Example: + + >>> 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 + >>> class MyMessage(models.Model): + ... embed = pgfields.JSONField( + ... validators=( + ... validate_tag_embed, + ... ) + ... ) + ... # ... + ... + + Args: + embed (Any): + A dictionary describing the contents of this embed. + See the official documentation for a full reference + of accepted keys by this dictionary: + https://discordapp.com/developers/docs/resources/channel#embed-object + + Raises: + ValidationError: + In case the given embed is deemed invalid, a `ValidationError` + is raised which in turn will allow Django to display errors + as appropriate. + """ + all_keys = { + 'title', 'type', 'description', 'url', 'timestamp', + 'color', 'footer', 'image', 'thumbnail', 'video', + 'provider', 'author', 'fields' + } + one_required_of = {'description', 'fields', 'image', 'title', 'video'} + field_validators = { + 'title': ( + MinLengthValidator( + limit_value=1, + message="Embed title must not be empty." + ), + MaxLengthValidator(limit_value=256) + ), + 'description': (MaxLengthValidator(limit_value=2048),), + 'fields': ( + MaxLengthValidator(limit_value=25), + validate_tag_embed_fields + ), + 'footer': (validate_tag_embed_footer,), + 'author': (validate_tag_embed_author,) + } + + if not embed: + raise ValidationError("Tag embed must not be empty.") + + elif not isinstance(embed, Mapping): + raise ValidationError("Tag embed must be a mapping.") + + elif not any(field in embed for field in one_required_of): + raise ValidationError(f"Tag embed must contain one of the fields {one_required_of}.") + + for required_key in one_required_of: + if required_key in embed and not embed[required_key]: + raise ValidationError(f"Key {required_key!r} must not be empty.") + + for field_name, value in embed.items(): + if field_name not in all_keys: + raise ValidationError(f"Unknown field name: {field_name!r}") + + if field_name in field_validators: + for validator in field_validators[field_name]: + validator(value) from django.db import models -- cgit v1.2.3 From 1f623b6629bdbc854323dc71d149a647355031c6 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 08:52:05 +0300 Subject: (Tag Cleanup): Removed `tag` from embed validator names due Tags will be removed. --- pydis_site/apps/api/models/mixins.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py index 2d658cb9..e95888b7 100644 --- a/pydis_site/apps/api/models/mixins.py +++ b/pydis_site/apps/api/models/mixins.py @@ -12,7 +12,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),), @@ -39,7 +39,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': ( @@ -64,7 +64,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': ( @@ -90,7 +90,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. @@ -106,11 +106,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, ... ) ... ) ... # ... @@ -146,10 +146,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: -- cgit v1.2.3 From 9b6b9d9d01811bbe2d9a1964970e3b4a38180623 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 08:55:09 +0300 Subject: (Tag Cleanup): Replaced import from Tags model with utils.py validator import. --- pydis_site/apps/api/models/bot/message.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/bot/message.py b/pydis_site/apps/api/models/bot/message.py index 78dcbf1d..e929ea25 100644 --- a/pydis_site/apps/api/models/bot/message.py +++ b/pydis_site/apps/api/models/bot/message.py @@ -5,9 +5,8 @@ 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 ModelReprMixin, validate_embed class Message(ModelReprMixin, models.Model): @@ -47,7 +46,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." -- cgit v1.2.3 From 5df2d613bc72aad60fed73d2703dff55093def03 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 09:02:48 +0300 Subject: (Tag Cleanup): Removed tag import in models __init__.py --- pydis_site/apps/api/models/__init__.py | 1 - 1 file changed, 1 deletion(-) (limited to 'pydis_site/apps/api/models') 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 -- cgit v1.2.3 From 494b735f762bf1780448db4591a4be4850dcdd4a Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 09:17:43 +0300 Subject: (Tag Cleanup): Removed Tag model --- .../apps/api/migrations/0019_deletedmessage.py | 2 +- pydis_site/apps/api/models/bot/tag.py | 25 ---------------------- 2 files changed, 1 insertion(+), 26 deletions(-) (limited to 'pydis_site/apps/api/models') 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/models/bot/tag.py b/pydis_site/apps/api/models/bot/tag.py index 5e53582f..790ad37a 100644 --- a/pydis_site/apps/api/models/bot/tag.py +++ b/pydis_site/apps/api/models/bot/tag.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: @@ -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 -- cgit v1.2.3 From a11d3a38a03b63a2018785917a56fe54c1b1f027 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 09:21:17 +0300 Subject: (Tag Cleanup): Removed Tag model file. --- pydis_site/apps/api/models/bot/tag.py | 173 ---------------------------------- 1 file changed, 173 deletions(-) delete mode 100644 pydis_site/apps/api/models/bot/tag.py (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/bot/tag.py b/pydis_site/apps/api/models/bot/tag.py deleted file mode 100644 index 790ad37a..00000000 --- a/pydis_site/apps/api/models/bot/tag.py +++ /dev/null @@ -1,173 +0,0 @@ -from collections.abc import Mapping -from typing import Any, Dict - -from django.core.exceptions import ValidationError -from django.core.validators import MaxLengthValidator, MinLengthValidator - - -def is_bool_validator(value: Any) -> None: - """Validates if a given value is of type bool.""" - if not isinstance(value, bool): - raise ValidationError(f"This field must be of type bool, not {type(value)}.") - - -def validate_tag_embed_fields(fields: dict) -> None: - """Raises a ValidationError if any of the given embed fields is invalid.""" - field_validators = { - 'name': (MaxLengthValidator(limit_value=256),), - 'value': (MaxLengthValidator(limit_value=1024),), - 'inline': (is_bool_validator,), - } - - required_fields = ('name', 'value') - - for field in fields: - if not isinstance(field, Mapping): - raise ValidationError("Embed fields must be a mapping.") - - if not all(required_field in field for required_field in required_fields): - raise ValidationError( - f"Embed fields must contain the following fields: {', '.join(required_fields)}." - ) - - for field_name, value in field.items(): - if field_name not in field_validators: - raise ValidationError(f"Unknown embed field field: {field_name!r}.") - - for validator in field_validators[field_name]: - validator(value) - - -def validate_tag_embed_footer(footer: Dict[str, str]) -> None: - """Raises a ValidationError if the given footer is invalid.""" - field_validators = { - 'text': ( - MinLengthValidator( - limit_value=1, - message="Footer text must not be empty." - ), - MaxLengthValidator(limit_value=2048) - ), - 'icon_url': (), - 'proxy_icon_url': () - } - - if not isinstance(footer, Mapping): - raise ValidationError("Embed footer must be a mapping.") - - for field_name, value in footer.items(): - if field_name not in field_validators: - raise ValidationError(f"Unknown embed footer field: {field_name!r}.") - - for validator in field_validators[field_name]: - validator(value) - - -def validate_tag_embed_author(author: Any) -> None: - """Raises a ValidationError if the given author is invalid.""" - field_validators = { - 'name': ( - MinLengthValidator( - limit_value=1, - message="Embed author name must not be empty." - ), - MaxLengthValidator(limit_value=256) - ), - 'url': (), - 'icon_url': (), - 'proxy_icon_url': () - } - - if not isinstance(author, Mapping): - raise ValidationError("Embed author must be a mapping.") - - for field_name, value in author.items(): - if field_name not in field_validators: - raise ValidationError(f"Unknown embed author field: {field_name!r}.") - - for validator in field_validators[field_name]: - validator(value) - - -def validate_tag_embed(embed: Any) -> None: - """ - Validate a JSON document containing an embed as possible to send on Discord. - - This attempts to rebuild the validation used by Discord - as well as possible by checking for various embed limits so we can - ensure that any embed we store here will also be accepted as a - valid embed by the Discord API. - - Using this directly is possible, although not intended - you usually - stick this onto the `validators` keyword argument of model fields. - - Example: - - >>> 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 - >>> class MyMessage(models.Model): - ... embed = pgfields.JSONField( - ... validators=( - ... validate_tag_embed, - ... ) - ... ) - ... # ... - ... - - Args: - embed (Any): - A dictionary describing the contents of this embed. - See the official documentation for a full reference - of accepted keys by this dictionary: - https://discordapp.com/developers/docs/resources/channel#embed-object - - Raises: - ValidationError: - In case the given embed is deemed invalid, a `ValidationError` - is raised which in turn will allow Django to display errors - as appropriate. - """ - all_keys = { - 'title', 'type', 'description', 'url', 'timestamp', - 'color', 'footer', 'image', 'thumbnail', 'video', - 'provider', 'author', 'fields' - } - one_required_of = {'description', 'fields', 'image', 'title', 'video'} - field_validators = { - 'title': ( - MinLengthValidator( - limit_value=1, - message="Embed title must not be empty." - ), - MaxLengthValidator(limit_value=256) - ), - 'description': (MaxLengthValidator(limit_value=2048),), - 'fields': ( - MaxLengthValidator(limit_value=25), - validate_tag_embed_fields - ), - 'footer': (validate_tag_embed_footer,), - 'author': (validate_tag_embed_author,) - } - - if not embed: - raise ValidationError("Tag embed must not be empty.") - - elif not isinstance(embed, Mapping): - raise ValidationError("Tag embed must be a mapping.") - - elif not any(field in embed for field in one_required_of): - raise ValidationError(f"Tag embed must contain one of the fields {one_required_of}.") - - for required_key in one_required_of: - if required_key in embed and not embed[required_key]: - raise ValidationError(f"Key {required_key!r} must not be empty.") - - for field_name, value in embed.items(): - if field_name not in all_keys: - raise ValidationError(f"Unknown field name: {field_name!r}") - - if field_name in field_validators: - for validator in field_validators[field_name]: - validator(value) -- cgit v1.2.3 From 2b2f491e476c4564899c8716fde189da4a493287 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 27 Aug 2020 07:34:41 +0300 Subject: Move import to beginning of models mixins file --- pydis_site/apps/api/models/mixins.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py index e95888b7..692d14f7 100644 --- a/pydis_site/apps/api/models/mixins.py +++ b/pydis_site/apps/api/models/mixins.py @@ -4,6 +4,7 @@ from typing import Any, Dict from django.core.exceptions import ValidationError from django.core.validators import MaxLengthValidator, MinLengthValidator +from django.db import models def is_bool_validator(value: Any) -> None: @@ -173,8 +174,6 @@ def validate_embed(embed: Any) -> None: for validator in field_validators[field_name]: validator(value) -from django.db import models - class ModelReprMixin: """Mixin providing a `__repr__()` to display model class name and initialisation parameters.""" -- cgit v1.2.3 From 2011d2f2c30dc67c1f34e82e5593f8e4ce1243dd Mon Sep 17 00:00:00 2001 From: Karlis S Date: Thu, 27 Aug 2020 04:44:08 +0000 Subject: Move some parts from Mixins file to utils --- pydis_site/apps/api/models/mixins.py | 172 ---------------------------------- pydis_site/apps/api/models/utils.py | 174 +++++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+), 172 deletions(-) create mode 100644 pydis_site/apps/api/models/utils.py (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py index 692d14f7..5d75b78b 100644 --- a/pydis_site/apps/api/models/mixins.py +++ b/pydis_site/apps/api/models/mixins.py @@ -1,180 +1,8 @@ -from collections.abc import Mapping from operator import itemgetter -from typing import Any, Dict -from django.core.exceptions import ValidationError -from django.core.validators import MaxLengthValidator, MinLengthValidator from django.db import models -def is_bool_validator(value: Any) -> None: - """Validates if a given value is of type bool.""" - if not isinstance(value, bool): - raise ValidationError(f"This field must be of type bool, not {type(value)}.") - - -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),), - 'value': (MaxLengthValidator(limit_value=1024),), - 'inline': (is_bool_validator,), - } - - required_fields = ('name', 'value') - - for field in fields: - if not isinstance(field, Mapping): - raise ValidationError("Embed fields must be a mapping.") - - if not all(required_field in field for required_field in required_fields): - raise ValidationError( - f"Embed fields must contain the following fields: {', '.join(required_fields)}." - ) - - for field_name, value in field.items(): - if field_name not in field_validators: - raise ValidationError(f"Unknown embed field field: {field_name!r}.") - - for validator in field_validators[field_name]: - validator(value) - - -def validate_embed_footer(footer: Dict[str, str]) -> None: - """Raises a ValidationError if the given footer is invalid.""" - field_validators = { - 'text': ( - MinLengthValidator( - limit_value=1, - message="Footer text must not be empty." - ), - MaxLengthValidator(limit_value=2048) - ), - 'icon_url': (), - 'proxy_icon_url': () - } - - if not isinstance(footer, Mapping): - raise ValidationError("Embed footer must be a mapping.") - - for field_name, value in footer.items(): - if field_name not in field_validators: - raise ValidationError(f"Unknown embed footer field: {field_name!r}.") - - for validator in field_validators[field_name]: - validator(value) - - -def validate_embed_author(author: Any) -> None: - """Raises a ValidationError if the given author is invalid.""" - field_validators = { - 'name': ( - MinLengthValidator( - limit_value=1, - message="Embed author name must not be empty." - ), - MaxLengthValidator(limit_value=256) - ), - 'url': (), - 'icon_url': (), - 'proxy_icon_url': () - } - - if not isinstance(author, Mapping): - raise ValidationError("Embed author must be a mapping.") - - for field_name, value in author.items(): - if field_name not in field_validators: - raise ValidationError(f"Unknown embed author field: {field_name!r}.") - - for validator in field_validators[field_name]: - validator(value) - - -def validate_embed(embed: Any) -> None: - """ - Validate a JSON document containing an embed as possible to send on Discord. - - This attempts to rebuild the validation used by Discord - as well as possible by checking for various embed limits so we can - ensure that any embed we store here will also be accepted as a - valid embed by the Discord API. - - Using this directly is possible, although not intended - you usually - stick this onto the `validators` keyword argument of model fields. - - Example: - - >>> from django.contrib.postgres import fields as pgfields - >>> from django.db import models - >>> from pydis_site.apps.api.models.utils import validate_embed - >>> class MyMessage(models.Model): - ... embed = pgfields.JSONField( - ... validators=( - ... validate_embed, - ... ) - ... ) - ... # ... - ... - - Args: - embed (Any): - A dictionary describing the contents of this embed. - See the official documentation for a full reference - of accepted keys by this dictionary: - https://discordapp.com/developers/docs/resources/channel#embed-object - - Raises: - ValidationError: - In case the given embed is deemed invalid, a `ValidationError` - is raised which in turn will allow Django to display errors - as appropriate. - """ - all_keys = { - 'title', 'type', 'description', 'url', 'timestamp', - 'color', 'footer', 'image', 'thumbnail', 'video', - 'provider', 'author', 'fields' - } - one_required_of = {'description', 'fields', 'image', 'title', 'video'} - field_validators = { - 'title': ( - MinLengthValidator( - limit_value=1, - message="Embed title must not be empty." - ), - MaxLengthValidator(limit_value=256) - ), - 'description': (MaxLengthValidator(limit_value=2048),), - 'fields': ( - MaxLengthValidator(limit_value=25), - validate_embed_fields - ), - 'footer': (validate_embed_footer,), - 'author': (validate_embed_author,) - } - - if not embed: - raise ValidationError("Tag embed must not be empty.") - - elif not isinstance(embed, Mapping): - raise ValidationError("Tag embed must be a mapping.") - - elif not any(field in embed for field in one_required_of): - raise ValidationError(f"Tag embed must contain one of the fields {one_required_of}.") - - for required_key in one_required_of: - if required_key in embed and not embed[required_key]: - raise ValidationError(f"Key {required_key!r} must not be empty.") - - for field_name, value in embed.items(): - if field_name not in all_keys: - raise ValidationError(f"Unknown field name: {field_name!r}") - - if field_name in field_validators: - for validator in field_validators[field_name]: - validator(value) - - class ModelReprMixin: """Mixin providing a `__repr__()` to display model class name and initialisation parameters.""" diff --git a/pydis_site/apps/api/models/utils.py b/pydis_site/apps/api/models/utils.py new file mode 100644 index 00000000..97a79507 --- /dev/null +++ b/pydis_site/apps/api/models/utils.py @@ -0,0 +1,174 @@ +from collections.abc import Mapping +from operator import itemgetter +from typing import Any, Dict + +from django.core.exceptions import ValidationError +from django.core.validators import MaxLengthValidator, MinLengthValidator + + +def is_bool_validator(value: Any) -> None: + """Validates if a given value is of type bool.""" + if not isinstance(value, bool): + raise ValidationError(f"This field must be of type bool, not {type(value)}.") + + +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),), + 'value': (MaxLengthValidator(limit_value=1024),), + 'inline': (is_bool_validator,), + } + + required_fields = ('name', 'value') + + for field in fields: + if not isinstance(field, Mapping): + raise ValidationError("Embed fields must be a mapping.") + + if not all(required_field in field for required_field in required_fields): + raise ValidationError( + f"Embed fields must contain the following fields: {', '.join(required_fields)}." + ) + + for field_name, value in field.items(): + if field_name not in field_validators: + raise ValidationError(f"Unknown embed field field: {field_name!r}.") + + for validator in field_validators[field_name]: + validator(value) + + +def validate_embed_footer(footer: Dict[str, str]) -> None: + """Raises a ValidationError if the given footer is invalid.""" + field_validators = { + 'text': ( + MinLengthValidator( + limit_value=1, + message="Footer text must not be empty." + ), + MaxLengthValidator(limit_value=2048) + ), + 'icon_url': (), + 'proxy_icon_url': () + } + + if not isinstance(footer, Mapping): + raise ValidationError("Embed footer must be a mapping.") + + for field_name, value in footer.items(): + if field_name not in field_validators: + raise ValidationError(f"Unknown embed footer field: {field_name!r}.") + + for validator in field_validators[field_name]: + validator(value) + + +def validate_embed_author(author: Any) -> None: + """Raises a ValidationError if the given author is invalid.""" + field_validators = { + 'name': ( + MinLengthValidator( + limit_value=1, + message="Embed author name must not be empty." + ), + MaxLengthValidator(limit_value=256) + ), + 'url': (), + 'icon_url': (), + 'proxy_icon_url': () + } + + if not isinstance(author, Mapping): + raise ValidationError("Embed author must be a mapping.") + + for field_name, value in author.items(): + if field_name not in field_validators: + raise ValidationError(f"Unknown embed author field: {field_name!r}.") + + for validator in field_validators[field_name]: + validator(value) + + +def validate_embed(embed: Any) -> None: + """ + Validate a JSON document containing an embed as possible to send on Discord. + + This attempts to rebuild the validation used by Discord + as well as possible by checking for various embed limits so we can + ensure that any embed we store here will also be accepted as a + valid embed by the Discord API. + + Using this directly is possible, although not intended - you usually + stick this onto the `validators` keyword argument of model fields. + + Example: + + >>> from django.contrib.postgres import fields as pgfields + >>> from django.db import models + >>> from pydis_site.apps.api.models.utils import validate_embed + >>> class MyMessage(models.Model): + ... embed = pgfields.JSONField( + ... validators=( + ... validate_embed, + ... ) + ... ) + ... # ... + ... + + Args: + embed (Any): + A dictionary describing the contents of this embed. + See the official documentation for a full reference + of accepted keys by this dictionary: + https://discordapp.com/developers/docs/resources/channel#embed-object + + Raises: + ValidationError: + In case the given embed is deemed invalid, a `ValidationError` + is raised which in turn will allow Django to display errors + as appropriate. + """ + all_keys = { + 'title', 'type', 'description', 'url', 'timestamp', + 'color', 'footer', 'image', 'thumbnail', 'video', + 'provider', 'author', 'fields' + } + one_required_of = {'description', 'fields', 'image', 'title', 'video'} + field_validators = { + 'title': ( + MinLengthValidator( + limit_value=1, + message="Embed title must not be empty." + ), + MaxLengthValidator(limit_value=256) + ), + 'description': (MaxLengthValidator(limit_value=2048),), + 'fields': ( + MaxLengthValidator(limit_value=25), + validate_embed_fields + ), + 'footer': (validate_embed_footer,), + 'author': (validate_embed_author,) + } + + if not embed: + raise ValidationError("Tag embed must not be empty.") + + elif not isinstance(embed, Mapping): + raise ValidationError("Tag embed must be a mapping.") + + elif not any(field in embed for field in one_required_of): + raise ValidationError(f"Tag embed must contain one of the fields {one_required_of}.") + + for required_key in one_required_of: + if required_key in embed and not embed[required_key]: + raise ValidationError(f"Key {required_key!r} must not be empty.") + + for field_name, value in embed.items(): + if field_name not in all_keys: + raise ValidationError(f"Unknown field name: {field_name!r}") + + if field_name in field_validators: + for validator in field_validators[field_name]: + validator(value) -- cgit v1.2.3 From 1ab4ccbbbcf38dddace5855ed9521219821b7698 Mon Sep 17 00:00:00 2001 From: Karlis S Date: Thu, 27 Aug 2020 04:48:09 +0000 Subject: Remove unused import from models utils --- pydis_site/apps/api/models/utils.py | 1 - 1 file changed, 1 deletion(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/utils.py b/pydis_site/apps/api/models/utils.py index 97a79507..107231ba 100644 --- a/pydis_site/apps/api/models/utils.py +++ b/pydis_site/apps/api/models/utils.py @@ -1,5 +1,4 @@ from collections.abc import Mapping -from operator import itemgetter from typing import Any, Dict from django.core.exceptions import ValidationError -- cgit v1.2.3 From fd2909c936efb9ab2285896b297b405c4dd7a1fb Mon Sep 17 00:00:00 2001 From: Karlis S Date: Thu, 27 Aug 2020 05:35:01 +0000 Subject: Move last parts from mixins to utils and delete mixins --- pydis_site/apps/api/models/mixins.py | 31 ------------------------------- pydis_site/apps/api/models/utils.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 31 deletions(-) delete mode 100644 pydis_site/apps/api/models/mixins.py (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py deleted file mode 100644 index 5d75b78b..00000000 --- a/pydis_site/apps/api/models/mixins.py +++ /dev/null @@ -1,31 +0,0 @@ -from operator import itemgetter - -from django.db import models - - -class ModelReprMixin: - """Mixin providing a `__repr__()` to display model class name and initialisation parameters.""" - - def __repr__(self): - """Returns the current model class name and initialisation parameters.""" - attributes = ' '.join( - f'{attribute}={value!r}' - for attribute, value in sorted( - self.__dict__.items(), - key=itemgetter(0) - ) - if not attribute.startswith('_') - ) - return f'<{self.__class__.__name__}({attributes})>' - - -class ModelTimestampMixin(models.Model): - """Mixin providing created_at and updated_at fields.""" - - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - """Metaconfig for the mixin.""" - - abstract = True diff --git a/pydis_site/apps/api/models/utils.py b/pydis_site/apps/api/models/utils.py index 107231ba..692d14f7 100644 --- a/pydis_site/apps/api/models/utils.py +++ b/pydis_site/apps/api/models/utils.py @@ -1,8 +1,10 @@ from collections.abc import Mapping +from operator import itemgetter from typing import Any, Dict from django.core.exceptions import ValidationError from django.core.validators import MaxLengthValidator, MinLengthValidator +from django.db import models def is_bool_validator(value: Any) -> None: @@ -171,3 +173,31 @@ def validate_embed(embed: Any) -> None: if field_name in field_validators: for validator in field_validators[field_name]: validator(value) + + +class ModelReprMixin: + """Mixin providing a `__repr__()` to display model class name and initialisation parameters.""" + + def __repr__(self): + """Returns the current model class name and initialisation parameters.""" + attributes = ' '.join( + f'{attribute}={value!r}' + for attribute, value in sorted( + self.__dict__.items(), + key=itemgetter(0) + ) + if not attribute.startswith('_') + ) + return f'<{self.__class__.__name__}({attributes})>' + + +class ModelTimestampMixin(models.Model): + """Mixin providing created_at and updated_at fields.""" + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + """Metaconfig for the mixin.""" + + abstract = True -- cgit v1.2.3 From 4eeb841724e7603dffc5334a6e4aa7b7b7ced997 Mon Sep 17 00:00:00 2001 From: Karlis S Date: Thu, 27 Aug 2020 05:37:55 +0000 Subject: Fix FilterList model mixins import path --- pydis_site/apps/api/models/bot/filter_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/bot/filter_list.py b/pydis_site/apps/api/models/bot/filter_list.py index d279e137..cb4acb68 100644 --- a/pydis_site/apps/api/models/bot/filter_list.py +++ b/pydis_site/apps/api/models/bot/filter_list.py @@ -1,6 +1,6 @@ from django.db import models -from pydis_site.apps.api.models.mixins import ModelReprMixin, ModelTimestampMixin +from pydis_site.apps.api.models.utils import ModelReprMixin, ModelTimestampMixin class FilterList(ModelTimestampMixin, ModelReprMixin, models.Model): -- cgit v1.2.3 From feafa0950d9132350e36f58fbae57121363d3277 Mon Sep 17 00:00:00 2001 From: Karlis S Date: Thu, 27 Aug 2020 05:47:47 +0000 Subject: Still move mixins back to its own file --- pydis_site/apps/api/models/bot/filter_list.py | 2 +- pydis_site/apps/api/models/mixins.py | 31 +++++++++++++++++++++++++++ pydis_site/apps/api/models/utils.py | 30 -------------------------- 3 files changed, 32 insertions(+), 31 deletions(-) create mode 100644 pydis_site/apps/api/models/mixins.py (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/bot/filter_list.py b/pydis_site/apps/api/models/bot/filter_list.py index cb4acb68..d279e137 100644 --- a/pydis_site/apps/api/models/bot/filter_list.py +++ b/pydis_site/apps/api/models/bot/filter_list.py @@ -1,6 +1,6 @@ from django.db import models -from pydis_site.apps.api.models.utils import ModelReprMixin, ModelTimestampMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin, ModelTimestampMixin class FilterList(ModelTimestampMixin, ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py new file mode 100644 index 00000000..5d75b78b --- /dev/null +++ b/pydis_site/apps/api/models/mixins.py @@ -0,0 +1,31 @@ +from operator import itemgetter + +from django.db import models + + +class ModelReprMixin: + """Mixin providing a `__repr__()` to display model class name and initialisation parameters.""" + + def __repr__(self): + """Returns the current model class name and initialisation parameters.""" + attributes = ' '.join( + f'{attribute}={value!r}' + for attribute, value in sorted( + self.__dict__.items(), + key=itemgetter(0) + ) + if not attribute.startswith('_') + ) + return f'<{self.__class__.__name__}({attributes})>' + + +class ModelTimestampMixin(models.Model): + """Mixin providing created_at and updated_at fields.""" + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + """Metaconfig for the mixin.""" + + abstract = True diff --git a/pydis_site/apps/api/models/utils.py b/pydis_site/apps/api/models/utils.py index 692d14f7..107231ba 100644 --- a/pydis_site/apps/api/models/utils.py +++ b/pydis_site/apps/api/models/utils.py @@ -1,10 +1,8 @@ from collections.abc import Mapping -from operator import itemgetter from typing import Any, Dict from django.core.exceptions import ValidationError from django.core.validators import MaxLengthValidator, MinLengthValidator -from django.db import models def is_bool_validator(value: Any) -> None: @@ -173,31 +171,3 @@ def validate_embed(embed: Any) -> None: if field_name in field_validators: for validator in field_validators[field_name]: validator(value) - - -class ModelReprMixin: - """Mixin providing a `__repr__()` to display model class name and initialisation parameters.""" - - def __repr__(self): - """Returns the current model class name and initialisation parameters.""" - attributes = ' '.join( - f'{attribute}={value!r}' - for attribute, value in sorted( - self.__dict__.items(), - key=itemgetter(0) - ) - if not attribute.startswith('_') - ) - return f'<{self.__class__.__name__}({attributes})>' - - -class ModelTimestampMixin(models.Model): - """Mixin providing created_at and updated_at fields.""" - - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - """Metaconfig for the mixin.""" - - abstract = True -- cgit v1.2.3 From e1f75feb74c24b262b276534070e4ffb3bf295b3 Mon Sep 17 00:00:00 2001 From: Karlis S Date: Thu, 27 Aug 2020 05:53:09 +0000 Subject: Fix import paths of mixins in message model --- pydis_site/apps/api/models/bot/message.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/bot/message.py b/pydis_site/apps/api/models/bot/message.py index e929ea25..f6ae55a5 100644 --- a/pydis_site/apps/api/models/bot/message.py +++ b/pydis_site/apps/api/models/bot/message.py @@ -6,7 +6,8 @@ from django.db import models from django.utils import timezone from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.utils import ModelReprMixin, validate_embed +from pydis_site.apps.api.models.mixins import ModelReprMixin +from pydis_site.apps.api.models.utils import validate_embed class Message(ModelReprMixin, models.Model): -- cgit v1.2.3 From dae68c46831ef14e6a0715add1025f9af1b5a73d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 29 Aug 2020 10:00:45 -0700 Subject: Revert pull request #348 Revert commit 330a27926a7f2a9bdae56a5928cd81e55ccb38ed to reverse changes made to 6c08da2f3e48db9c9dd452f2e0505c4a61e46592. Migrations were failing when those changes were deployed. --- .../api/migrations/0051_create_news_setting.py | 25 ++++ .../api/migrations/0052_create_news_setting.py | 25 ---- .../api/migrations/0052_remove_user_avatar_hash.py | 17 +++ .../migrations/0053_offtopicchannelname_used.py | 18 --- .../api/migrations/0053_user_roles_to_array.py | 24 ++++ .../api/migrations/0054_remove_user_avatar_hash.py | 17 --- .../0054_user_invalidate_unknown_role.py | 21 +++ .../api/migrations/0055_merge_20200714_2027.py | 14 ++ .../apps/api/migrations/0055_reminder_mentions.py | 20 +++ .../api/migrations/0055_user_roles_to_array.py | 24 ---- .../api/migrations/0056_allow_blank_user_roles.py | 21 +++ .../0056_user_invalidate_unknown_role.py | 21 --- .../api/migrations/0057_merge_20200716_0751.py | 14 ++ .../apps/api/migrations/0057_reminder_mentions.py | 20 --- .../api/migrations/0058_allow_blank_user_roles.py | 21 --- .../migrations/0058_create_new_filterlist_model.py | 33 +++++ .../migrations/0059_create_new_filterlist_model.py | 33 ----- .../api/migrations/0059_populate_filterlists.py | 153 +++++++++++++++++++++ .../api/migrations/0060_populate_filterlists.py | 153 --------------------- .../migrations/0060_populate_filterlists_fix.py | 85 ++++++++++++ .../migrations/0061_populate_filterlists_fix.py | 85 ------------ .../apps/api/models/bot/off_topic_channel_name.py | 5 - .../apps/api/tests/test_off_topic_channel_names.py | 27 +--- .../api/viewsets/bot/off_topic_channel_name.py | 27 +--- 24 files changed, 431 insertions(+), 472 deletions(-) create mode 100644 pydis_site/apps/api/migrations/0051_create_news_setting.py delete mode 100644 pydis_site/apps/api/migrations/0052_create_news_setting.py create mode 100644 pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py delete mode 100644 pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py create mode 100644 pydis_site/apps/api/migrations/0053_user_roles_to_array.py delete mode 100644 pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py create mode 100644 pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py create mode 100644 pydis_site/apps/api/migrations/0055_merge_20200714_2027.py create mode 100644 pydis_site/apps/api/migrations/0055_reminder_mentions.py delete mode 100644 pydis_site/apps/api/migrations/0055_user_roles_to_array.py create mode 100644 pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py delete mode 100644 pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py create mode 100644 pydis_site/apps/api/migrations/0057_merge_20200716_0751.py delete mode 100644 pydis_site/apps/api/migrations/0057_reminder_mentions.py delete mode 100644 pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py create mode 100644 pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py delete mode 100644 pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py create mode 100644 pydis_site/apps/api/migrations/0059_populate_filterlists.py delete mode 100644 pydis_site/apps/api/migrations/0060_populate_filterlists.py create mode 100644 pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py delete mode 100644 pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/migrations/0051_create_news_setting.py b/pydis_site/apps/api/migrations/0051_create_news_setting.py new file mode 100644 index 00000000..f18fdfb1 --- /dev/null +++ b/pydis_site/apps/api/migrations/0051_create_news_setting.py @@ -0,0 +1,25 @@ +from django.db import migrations + + +def up(apps, schema_editor): + BotSetting = apps.get_model('api', 'BotSetting') + setting = BotSetting( + name='news', + data={} + ).save() + + +def down(apps, schema_editor): + BotSetting = apps.get_model('api', 'BotSetting') + BotSetting.objects.get(name='news').delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0050_remove_infractions_active_default_value'), + ] + + operations = [ + migrations.RunPython(up, down) + ] diff --git a/pydis_site/apps/api/migrations/0052_create_news_setting.py b/pydis_site/apps/api/migrations/0052_create_news_setting.py deleted file mode 100644 index b101d19d..00000000 --- a/pydis_site/apps/api/migrations/0052_create_news_setting.py +++ /dev/null @@ -1,25 +0,0 @@ -from django.db import migrations - - -def up(apps, schema_editor): - BotSetting = apps.get_model('api', 'BotSetting') - setting = BotSetting( - name='news', - data={} - ).save() - - -def down(apps, schema_editor): - BotSetting = apps.get_model('api', 'BotSetting') - BotSetting.objects.get(name='news').delete() - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0051_allow_blank_message_embeds'), - ] - - operations = [ - migrations.RunPython(up, down) - ] diff --git a/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py new file mode 100644 index 00000000..26b3b954 --- /dev/null +++ b/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.11 on 2020-05-27 07:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0051_create_news_setting'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='avatar_hash', + ), + ] diff --git a/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py deleted file mode 100644 index b51ce1d2..00000000 --- a/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.11 on 2020-03-30 10:24 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0052_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/0053_user_roles_to_array.py b/pydis_site/apps/api/migrations/0053_user_roles_to_array.py new file mode 100644 index 00000000..7ff3a548 --- /dev/null +++ b/pydis_site/apps/api/migrations/0053_user_roles_to_array.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.11 on 2020-06-02 13:42 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0052_remove_user_avatar_hash'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='roles', + ), + migrations.AddField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py deleted file mode 100644 index be9fd948..00000000 --- a/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.2.11 on 2020-05-27 07:17 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0053_offtopicchannelname_used'), - ] - - operations = [ - migrations.RemoveField( - model_name='user', - name='avatar_hash', - ), - ] diff --git a/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py new file mode 100644 index 00000000..96230015 --- /dev/null +++ b/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.11 on 2020-06-02 20:08 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.user + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0053_user_roles_to_array'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py b/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py new file mode 100644 index 00000000..f2a0e638 --- /dev/null +++ b/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.8 on 2020-07-14 20:27 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0051_allow_blank_message_embeds'), + ('api', '0054_user_invalidate_unknown_role'), + ] + + operations = [ + ] diff --git a/pydis_site/apps/api/migrations/0055_reminder_mentions.py b/pydis_site/apps/api/migrations/0055_reminder_mentions.py new file mode 100644 index 00000000..d73b450d --- /dev/null +++ b/pydis_site/apps/api/migrations/0055_reminder_mentions.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.14 on 2020-07-15 07:37 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0054_user_invalidate_unknown_role'), + ] + + operations = [ + migrations.AddField( + model_name='reminder', + name='mentions', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0055_user_roles_to_array.py b/pydis_site/apps/api/migrations/0055_user_roles_to_array.py deleted file mode 100644 index e7b4a983..00000000 --- a/pydis_site/apps/api/migrations/0055_user_roles_to_array.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.2.11 on 2020-06-02 13:42 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0054_remove_user_avatar_hash'), - ] - - operations = [ - migrations.RemoveField( - model_name='user', - name='roles', - ), - migrations.AddField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py new file mode 100644 index 00000000..489941c7 --- /dev/null +++ b/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0.8 on 2020-07-14 20:35 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.user + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0055_merge_20200714_2027'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py deleted file mode 100644 index ab2696aa..00000000 --- a/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 2.2.11 on 2020-06-02 20:08 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models -import pydis_site.apps.api.models.bot.user - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0055_user_roles_to_array'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py b/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py new file mode 100644 index 00000000..47a6d2d4 --- /dev/null +++ b/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.14 on 2020-07-16 07:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0055_reminder_mentions'), + ('api', '0056_allow_blank_user_roles'), + ] + + operations = [ + ] diff --git a/pydis_site/apps/api/migrations/0057_reminder_mentions.py b/pydis_site/apps/api/migrations/0057_reminder_mentions.py deleted file mode 100644 index fb829a17..00000000 --- a/pydis_site/apps/api/migrations/0057_reminder_mentions.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.2.14 on 2020-07-15 07:37 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0056_user_invalidate_unknown_role'), - ] - - operations = [ - migrations.AddField( - model_name='reminder', - name='mentions', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py deleted file mode 100644 index 8f7fddfc..00000000 --- a/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-14 20:35 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models -import pydis_site.apps.api.models.bot.user - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0057_reminder_mentions'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py new file mode 100644 index 00000000..aecfdad7 --- /dev/null +++ b/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py @@ -0,0 +1,33 @@ +# Generated by Django 3.0.8 on 2020-07-15 11:23 + +from django.db import migrations, models +import pydis_site.apps.api.models.mixins + + +class Migration(migrations.Migration): + dependencies = [ + ('api', '0057_merge_20200716_0751'), + ] + + operations = [ + migrations.CreateModel( + name='FilterList', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('type', models.CharField( + choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), + ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], + help_text='The type of allowlist this is on.', max_length=50)), + ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), + ('content', models.TextField(help_text='The data to add to the allow or denylist.')), + ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), + ], + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), + ), + migrations.AddConstraint( + model_name='filterlist', + constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list') + ) + ] diff --git a/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py deleted file mode 100644 index eac5542d..00000000 --- a/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-15 11:23 - -from django.db import migrations, models -import pydis_site.apps.api.models.mixins - - -class Migration(migrations.Migration): - dependencies = [ - ('api', '0058_allow_blank_user_roles'), - ] - - operations = [ - migrations.CreateModel( - name='FilterList', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('type', models.CharField( - choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), - ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], - help_text='The type of allowlist this is on.', max_length=50)), - ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), - ('content', models.TextField(help_text='The data to add to the allow or denylist.')), - ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), - ], - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - migrations.AddConstraint( - model_name='filterlist', - constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list') - ) - ] diff --git a/pydis_site/apps/api/migrations/0059_populate_filterlists.py b/pydis_site/apps/api/migrations/0059_populate_filterlists.py new file mode 100644 index 00000000..8c550191 --- /dev/null +++ b/pydis_site/apps/api/migrations/0059_populate_filterlists.py @@ -0,0 +1,153 @@ +from django.db import migrations + +guild_invite_whitelist = [ + ("discord.gg/python", "Python Discord", True), + ("discord.gg/4JJdJKb", "RLBot", True), + ("discord.gg/djPtTRJ", "Kivy", True), + ("discord.gg/QXyegWe", "Pyglet", True), + ("discord.gg/9XsucTT", "Panda3D", True), + ("discord.gg/AP3rq2k", "PyWeek", True), + ("discord.gg/vSPsP9t", "Microsoft Python", True), + ("discord.gg/bRCvFy9", "Discord.js Official", True), + ("discord.gg/9zT7NHP", "Programming Discussions", True), + ("discord.gg/ysd6M4r", "JetBrains Community", True), + ("discord.gg/4xJeCgy", "Raspberry Pie", True), + ("discord.gg/AStb3kZ", "Ren'Py", True), + ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), + ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), + ("discord.gg/jTtgWuy", "Django", True), + ("discord.gg/W9BypZF", "STEM", True), + ("discord.gg/dpy", "discord.py", True), + ("discord.gg/programming", "Programmers Hangout", True), + ("discord.gg/qhGUjGD", "SpeakJS", True), + ("discord.gg/eTbWSZj", "Functional Programming", True), + ("discord.gg/r8yreB6", "PyGame", True), + ("discord.gg/5UBnR3P", "Python Atlanta", True), + ("discord.gg/ccyrDKv", "C#", True), +] + +domain_name_blacklist = [ + ("pornhub.com", None, False), + ("liveleak.com", None, False), + ("grabify.link", None, False), + ("bmwforum.co", None, False), + ("leancoding.co", None, False), + ("spottyfly.com", None, False), + ("stopify.co", None, False), + ("yoütu.be", None, False), + ("discörd.com", None, False), + ("minecräft.com", None, False), + ("freegiftcards.co", None, False), + ("disçordapp.com", None, False), + ("fortnight.space", None, False), + ("fortnitechat.site", None, False), + ("joinmy.site", None, False), + ("curiouscat.club", None, False), + ("catsnthings.fun", None, False), + ("yourtube.site", None, False), + ("youtubeshort.watch", None, False), + ("catsnthing.com", None, False), + ("youtubeshort.pro", None, False), + ("canadianlumberjacks.online", None, False), + ("poweredbydialup.club", None, False), + ("poweredbydialup.online", None, False), + ("poweredbysecurity.org", None, False), + ("poweredbysecurity.online", None, False), + ("ssteam.site", None, False), + ("steamwalletgift.com", None, False), + ("discord.gift", None, False), + ("lmgtfy.com", None, False), +] + +filter_token_blacklist = [ + ("\bgoo+ks*\b", None, False), + ("\bky+s+\b", None, False), + ("\bki+ke+s*\b", None, False), + ("\bbeaner+s?\b", None, False), + ("\bcoo+ns*\b", None, False), + ("\bnig+lets*\b", None, False), + ("\bslant-eyes*\b", None, False), + ("\btowe?l-?head+s*\b", None, False), + ("\bchi*n+k+s*\b", None, False), + ("\bspick*s*\b", None, False), + ("\bkill* +(?:yo)?urself+\b", None, False), + ("\bjew+s*\b", None, False), + ("\bsuicide\b", None, False), + ("\brape\b", None, False), + ("\b(re+)tar+(d+|t+)(ed)?\b", None, False), + ("\bta+r+d+\b", None, False), + ("\bcunts*\b", None, False), + ("\btrann*y\b", None, False), + ("\bshemale\b", None, False), + ("fa+g+s*", None, False), + ("卐", None, False), + ("卍", None, False), + ("࿖", None, False), + ("࿕", None, False), + ("࿘", None, False), + ("࿗", None, False), + ("cuck(?!oo+)", None, False), + ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), + ("fag+o+t+s*", None, False), +] + +file_format_whitelist = [ + (".3gp", None, True), + (".3g2", None, True), + (".avi", None, True), + (".bmp", None, True), + (".gif", None, True), + (".h264", None, True), + (".jpg", None, True), + (".jpeg", None, True), + (".m4v", None, True), + (".mkv", None, True), + (".mov", None, True), + (".mp4", None, True), + (".mpeg", None, True), + (".mpg", None, True), + (".png", None, True), + (".tiff", None, True), + (".wmv", None, True), + (".svg", None, True), + (".psd", "Photoshop", True), + (".ai", "Illustrator", True), + (".aep", "After Effects", True), + (".xcf", "GIMP", True), + (".mp3", None, True), + (".wav", None, True), + (".ogg", None, True), + (".webm", None, True), + (".webp", None, True), +] + +populate_data = { + "FILTER_TOKEN": filter_token_blacklist, + "DOMAIN_NAME": domain_name_blacklist, + "FILE_FORMAT": file_format_whitelist, + "GUILD_INVITE": guild_invite_whitelist, +} + + +class Migration(migrations.Migration): + dependencies = [("api", "0058_create_new_filterlist_model")] + + def populate_filterlists(app, _): + FilterList = app.get_model("api", "FilterList") + + for filterlist_type, metadata in populate_data.items(): + for content, comment, allowed in metadata: + FilterList.objects.create( + type=filterlist_type, + allowed=allowed, + content=content, + comment=comment, + ) + + def clear_filterlists(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.all().delete() + + operations = [ + migrations.RunPython(populate_filterlists, clear_filterlists) + ] diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists.py b/pydis_site/apps/api/migrations/0060_populate_filterlists.py deleted file mode 100644 index 35fde95a..00000000 --- a/pydis_site/apps/api/migrations/0060_populate_filterlists.py +++ /dev/null @@ -1,153 +0,0 @@ -from django.db import migrations - -guild_invite_whitelist = [ - ("discord.gg/python", "Python Discord", True), - ("discord.gg/4JJdJKb", "RLBot", True), - ("discord.gg/djPtTRJ", "Kivy", True), - ("discord.gg/QXyegWe", "Pyglet", True), - ("discord.gg/9XsucTT", "Panda3D", True), - ("discord.gg/AP3rq2k", "PyWeek", True), - ("discord.gg/vSPsP9t", "Microsoft Python", True), - ("discord.gg/bRCvFy9", "Discord.js Official", True), - ("discord.gg/9zT7NHP", "Programming Discussions", True), - ("discord.gg/ysd6M4r", "JetBrains Community", True), - ("discord.gg/4xJeCgy", "Raspberry Pie", True), - ("discord.gg/AStb3kZ", "Ren'Py", True), - ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), - ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), - ("discord.gg/jTtgWuy", "Django", True), - ("discord.gg/W9BypZF", "STEM", True), - ("discord.gg/dpy", "discord.py", True), - ("discord.gg/programming", "Programmers Hangout", True), - ("discord.gg/qhGUjGD", "SpeakJS", True), - ("discord.gg/eTbWSZj", "Functional Programming", True), - ("discord.gg/r8yreB6", "PyGame", True), - ("discord.gg/5UBnR3P", "Python Atlanta", True), - ("discord.gg/ccyrDKv", "C#", True), -] - -domain_name_blacklist = [ - ("pornhub.com", None, False), - ("liveleak.com", None, False), - ("grabify.link", None, False), - ("bmwforum.co", None, False), - ("leancoding.co", None, False), - ("spottyfly.com", None, False), - ("stopify.co", None, False), - ("yoütu.be", None, False), - ("discörd.com", None, False), - ("minecräft.com", None, False), - ("freegiftcards.co", None, False), - ("disçordapp.com", None, False), - ("fortnight.space", None, False), - ("fortnitechat.site", None, False), - ("joinmy.site", None, False), - ("curiouscat.club", None, False), - ("catsnthings.fun", None, False), - ("yourtube.site", None, False), - ("youtubeshort.watch", None, False), - ("catsnthing.com", None, False), - ("youtubeshort.pro", None, False), - ("canadianlumberjacks.online", None, False), - ("poweredbydialup.club", None, False), - ("poweredbydialup.online", None, False), - ("poweredbysecurity.org", None, False), - ("poweredbysecurity.online", None, False), - ("ssteam.site", None, False), - ("steamwalletgift.com", None, False), - ("discord.gift", None, False), - ("lmgtfy.com", None, False), -] - -filter_token_blacklist = [ - ("\bgoo+ks*\b", None, False), - ("\bky+s+\b", None, False), - ("\bki+ke+s*\b", None, False), - ("\bbeaner+s?\b", None, False), - ("\bcoo+ns*\b", None, False), - ("\bnig+lets*\b", None, False), - ("\bslant-eyes*\b", None, False), - ("\btowe?l-?head+s*\b", None, False), - ("\bchi*n+k+s*\b", None, False), - ("\bspick*s*\b", None, False), - ("\bkill* +(?:yo)?urself+\b", None, False), - ("\bjew+s*\b", None, False), - ("\bsuicide\b", None, False), - ("\brape\b", None, False), - ("\b(re+)tar+(d+|t+)(ed)?\b", None, False), - ("\bta+r+d+\b", None, False), - ("\bcunts*\b", None, False), - ("\btrann*y\b", None, False), - ("\bshemale\b", None, False), - ("fa+g+s*", None, False), - ("卐", None, False), - ("卍", None, False), - ("࿖", None, False), - ("࿕", None, False), - ("࿘", None, False), - ("࿗", None, False), - ("cuck(?!oo+)", None, False), - ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), - ("fag+o+t+s*", None, False), -] - -file_format_whitelist = [ - (".3gp", None, True), - (".3g2", None, True), - (".avi", None, True), - (".bmp", None, True), - (".gif", None, True), - (".h264", None, True), - (".jpg", None, True), - (".jpeg", None, True), - (".m4v", None, True), - (".mkv", None, True), - (".mov", None, True), - (".mp4", None, True), - (".mpeg", None, True), - (".mpg", None, True), - (".png", None, True), - (".tiff", None, True), - (".wmv", None, True), - (".svg", None, True), - (".psd", "Photoshop", True), - (".ai", "Illustrator", True), - (".aep", "After Effects", True), - (".xcf", "GIMP", True), - (".mp3", None, True), - (".wav", None, True), - (".ogg", None, True), - (".webm", None, True), - (".webp", None, True), -] - -populate_data = { - "FILTER_TOKEN": filter_token_blacklist, - "DOMAIN_NAME": domain_name_blacklist, - "FILE_FORMAT": file_format_whitelist, - "GUILD_INVITE": guild_invite_whitelist, -} - - -class Migration(migrations.Migration): - dependencies = [("api", "0059_create_new_filterlist_model")] - - def populate_filterlists(app, _): - FilterList = app.get_model("api", "FilterList") - - for filterlist_type, metadata in populate_data.items(): - for content, comment, allowed in metadata: - FilterList.objects.create( - type=filterlist_type, - allowed=allowed, - content=content, - comment=comment, - ) - - def clear_filterlists(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.all().delete() - - operations = [ - migrations.RunPython(populate_filterlists, clear_filterlists) - ] diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py new file mode 100644 index 00000000..53846f02 --- /dev/null +++ b/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py @@ -0,0 +1,85 @@ +from django.db import migrations + +bad_guild_invite_whitelist = [ + ("discord.gg/python", "Python Discord", True), + ("discord.gg/4JJdJKb", "RLBot", True), + ("discord.gg/djPtTRJ", "Kivy", True), + ("discord.gg/QXyegWe", "Pyglet", True), + ("discord.gg/9XsucTT", "Panda3D", True), + ("discord.gg/AP3rq2k", "PyWeek", True), + ("discord.gg/vSPsP9t", "Microsoft Python", True), + ("discord.gg/bRCvFy9", "Discord.js Official", True), + ("discord.gg/9zT7NHP", "Programming Discussions", True), + ("discord.gg/ysd6M4r", "JetBrains Community", True), + ("discord.gg/4xJeCgy", "Raspberry Pie", True), + ("discord.gg/AStb3kZ", "Ren'Py", True), + ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), + ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), + ("discord.gg/jTtgWuy", "Django", True), + ("discord.gg/W9BypZF", "STEM", True), + ("discord.gg/dpy", "discord.py", True), + ("discord.gg/programming", "Programmers Hangout", True), + ("discord.gg/qhGUjGD", "SpeakJS", True), + ("discord.gg/eTbWSZj", "Functional Programming", True), + ("discord.gg/r8yreB6", "PyGame", True), + ("discord.gg/5UBnR3P", "Python Atlanta", True), + ("discord.gg/ccyrDKv", "C#", True), +] + +guild_invite_whitelist = [ + ("267624335836053506", "Python Discord", True), + ("348658686962696195", "RLBot", True), + ("423249981340778496", "Kivy", True), + ("438622377094414346", "Pyglet", True), + ("524691714909274162", "Panda3D", True), + ("666560367173828639", "PyWeek", True), + ("702724176489873509", "Microsoft Python", True), + ("222078108977594368", "Discord.js Official", True), + ("238666723824238602", "Programming Discussions", True), + ("433980600391696384", "JetBrains Community", True), + ("204621105720328193", "Raspberry Pie", True), + ("286633898581164032", "Ren'Py", True), + ("440186186024222721", "Python Discord: Emojis 1", True), + ("578587418123304970", "Python Discord: Emojis 2", True), + ("159039020565790721", "Django", True), + ("273944235143593984", "STEM", True), + ("336642139381301249", "discord.py", True), + ("244230771232079873", "Programmers Hangout", True), + ("239433591950540801", "SpeakJS", True), + ("280033776820813825", "Functional Programming", True), + ("349505959032389632", "PyGame", True), + ("488751051629920277", "Python Atlanta", True), + ("143867839282020352", "C#", True), +] + + +class Migration(migrations.Migration): + dependencies = [("api", "0059_populate_filterlists")] + + def fix_filterlist(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.filter(type="GUILD_INVITE").delete() # Clear out the stuff added in 0059. + + for content, comment, allowed in guild_invite_whitelist: + FilterList.objects.create( + type="GUILD_INVITE", + allowed=allowed, + content=content, + comment=comment, + ) + + def restore_bad_filterlist(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.filter(type="GUILD_INVITE").delete() + + for content, comment, allowed in bad_guild_invite_whitelist: + FilterList.objects.create( + type="GUILD_INVITE", + allowed=allowed, + content=content, + comment=comment, + ) + + operations = [ + migrations.RunPython(fix_filterlist, restore_bad_filterlist) + ] diff --git a/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py deleted file mode 100644 index eaaafb38..00000000 --- a/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py +++ /dev/null @@ -1,85 +0,0 @@ -from django.db import migrations - -bad_guild_invite_whitelist = [ - ("discord.gg/python", "Python Discord", True), - ("discord.gg/4JJdJKb", "RLBot", True), - ("discord.gg/djPtTRJ", "Kivy", True), - ("discord.gg/QXyegWe", "Pyglet", True), - ("discord.gg/9XsucTT", "Panda3D", True), - ("discord.gg/AP3rq2k", "PyWeek", True), - ("discord.gg/vSPsP9t", "Microsoft Python", True), - ("discord.gg/bRCvFy9", "Discord.js Official", True), - ("discord.gg/9zT7NHP", "Programming Discussions", True), - ("discord.gg/ysd6M4r", "JetBrains Community", True), - ("discord.gg/4xJeCgy", "Raspberry Pie", True), - ("discord.gg/AStb3kZ", "Ren'Py", True), - ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), - ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), - ("discord.gg/jTtgWuy", "Django", True), - ("discord.gg/W9BypZF", "STEM", True), - ("discord.gg/dpy", "discord.py", True), - ("discord.gg/programming", "Programmers Hangout", True), - ("discord.gg/qhGUjGD", "SpeakJS", True), - ("discord.gg/eTbWSZj", "Functional Programming", True), - ("discord.gg/r8yreB6", "PyGame", True), - ("discord.gg/5UBnR3P", "Python Atlanta", True), - ("discord.gg/ccyrDKv", "C#", True), -] - -guild_invite_whitelist = [ - ("267624335836053506", "Python Discord", True), - ("348658686962696195", "RLBot", True), - ("423249981340778496", "Kivy", True), - ("438622377094414346", "Pyglet", True), - ("524691714909274162", "Panda3D", True), - ("666560367173828639", "PyWeek", True), - ("702724176489873509", "Microsoft Python", True), - ("222078108977594368", "Discord.js Official", True), - ("238666723824238602", "Programming Discussions", True), - ("433980600391696384", "JetBrains Community", True), - ("204621105720328193", "Raspberry Pie", True), - ("286633898581164032", "Ren'Py", True), - ("440186186024222721", "Python Discord: Emojis 1", True), - ("578587418123304970", "Python Discord: Emojis 2", True), - ("159039020565790721", "Django", True), - ("273944235143593984", "STEM", True), - ("336642139381301249", "discord.py", True), - ("244230771232079873", "Programmers Hangout", True), - ("239433591950540801", "SpeakJS", True), - ("280033776820813825", "Functional Programming", True), - ("349505959032389632", "PyGame", True), - ("488751051629920277", "Python Atlanta", True), - ("143867839282020352", "C#", True), -] - - -class Migration(migrations.Migration): - dependencies = [("api", "0060_populate_filterlists")] - - def fix_filterlist(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.filter(type="GUILD_INVITE").delete() # Clear out the stuff added in 0059. - - for content, comment, allowed in guild_invite_whitelist: - FilterList.objects.create( - type="GUILD_INVITE", - allowed=allowed, - content=content, - comment=comment, - ) - - def restore_bad_filterlist(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.filter(type="GUILD_INVITE").delete() - - for content, comment, allowed in bad_guild_invite_whitelist: - FilterList.objects.create( - type="GUILD_INVITE", - allowed=allowed, - content=content, - comment=comment, - ) - - operations = [ - migrations.RunPython(fix_filterlist, restore_bad_filterlist) - ] 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 403c7465..20e77b9f 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,11 +16,6 @@ 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/tests/test_off_topic_channel_names.py b/pydis_site/apps/api/tests/test_off_topic_channel_names.py index 3ab8b22d..bd42cd81 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,14 +10,12 @@ 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') @@ -26,7 +24,6 @@ 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) @@ -34,7 +31,6 @@ 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') @@ -42,7 +38,6 @@ 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') @@ -52,7 +47,6 @@ 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') @@ -65,11 +59,10 @@ class EmptyDatabaseTests(APISubdomainTestCase): class ListTests(APISubdomainTestCase): @classmethod def setUpTestData(cls): - 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) + cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand') + cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk') 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) @@ -83,21 +76,11 @@ 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): @@ -110,7 +93,6 @@ 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 = ( '𝖠𝖡𝖢𝖣𝖤𝖥𝖦𝖧𝖨𝖩𝖪𝖫𝖬𝖭𝖮𝖯𝖰𝖱𝖲𝖳𝖴𝖵𝖶𝖷𝖸𝖹', @@ -122,7 +104,6 @@ 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) @@ -131,7 +112,6 @@ 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', @@ -154,21 +134,18 @@ 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/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py index 826ad25e..d6da2399 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,4 +1,3 @@ -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 @@ -21,9 +20,7 @@ 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 - that is not used in current rotation. - When running out of names, API will mark all names to not used and start new rotation. + ... then the API will return `5` random items from the database. #### Response format Return a list of off-topic-channel names: @@ -109,27 +106,7 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): 'random_items': ["Must be a positive integer."] }) - 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) - + queryset = self.get_queryset().order_by('?')[:random_count] serialized = self.serializer_class(queryset, many=True) return Response(serialized.data) -- cgit v1.2.3 From c1ec2f1f9fe443cdde3750b1df1c3d969df3e67e Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 30 Aug 2020 08:20:25 +0300 Subject: Revert "Revert pull request #348" This reverts commit dae68c46831ef14e6a0715add1025f9af1b5a73d. --- .../api/migrations/0051_create_news_setting.py | 25 ---- .../api/migrations/0052_create_news_setting.py | 25 ++++ .../api/migrations/0052_remove_user_avatar_hash.py | 17 --- .../migrations/0053_offtopicchannelname_used.py | 18 +++ .../api/migrations/0053_user_roles_to_array.py | 24 ---- .../api/migrations/0054_remove_user_avatar_hash.py | 17 +++ .../0054_user_invalidate_unknown_role.py | 21 --- .../api/migrations/0055_merge_20200714_2027.py | 14 -- .../apps/api/migrations/0055_reminder_mentions.py | 20 --- .../api/migrations/0055_user_roles_to_array.py | 24 ++++ .../api/migrations/0056_allow_blank_user_roles.py | 21 --- .../0056_user_invalidate_unknown_role.py | 21 +++ .../api/migrations/0057_merge_20200716_0751.py | 14 -- .../apps/api/migrations/0057_reminder_mentions.py | 20 +++ .../api/migrations/0058_allow_blank_user_roles.py | 21 +++ .../migrations/0058_create_new_filterlist_model.py | 33 ----- .../migrations/0059_create_new_filterlist_model.py | 33 +++++ .../api/migrations/0059_populate_filterlists.py | 153 --------------------- .../api/migrations/0060_populate_filterlists.py | 153 +++++++++++++++++++++ .../migrations/0060_populate_filterlists_fix.py | 85 ------------ .../migrations/0061_populate_filterlists_fix.py | 85 ++++++++++++ .../apps/api/models/bot/off_topic_channel_name.py | 5 + .../apps/api/tests/test_off_topic_channel_names.py | 27 +++- .../api/viewsets/bot/off_topic_channel_name.py | 27 +++- 24 files changed, 472 insertions(+), 431 deletions(-) delete mode 100644 pydis_site/apps/api/migrations/0051_create_news_setting.py create mode 100644 pydis_site/apps/api/migrations/0052_create_news_setting.py delete mode 100644 pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py create mode 100644 pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py delete mode 100644 pydis_site/apps/api/migrations/0053_user_roles_to_array.py create mode 100644 pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py delete mode 100644 pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py delete mode 100644 pydis_site/apps/api/migrations/0055_merge_20200714_2027.py delete mode 100644 pydis_site/apps/api/migrations/0055_reminder_mentions.py create mode 100644 pydis_site/apps/api/migrations/0055_user_roles_to_array.py delete mode 100644 pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py create mode 100644 pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py delete mode 100644 pydis_site/apps/api/migrations/0057_merge_20200716_0751.py create mode 100644 pydis_site/apps/api/migrations/0057_reminder_mentions.py create mode 100644 pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py delete mode 100644 pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py create mode 100644 pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py delete mode 100644 pydis_site/apps/api/migrations/0059_populate_filterlists.py create mode 100644 pydis_site/apps/api/migrations/0060_populate_filterlists.py delete mode 100644 pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py create mode 100644 pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/migrations/0051_create_news_setting.py b/pydis_site/apps/api/migrations/0051_create_news_setting.py deleted file mode 100644 index f18fdfb1..00000000 --- a/pydis_site/apps/api/migrations/0051_create_news_setting.py +++ /dev/null @@ -1,25 +0,0 @@ -from django.db import migrations - - -def up(apps, schema_editor): - BotSetting = apps.get_model('api', 'BotSetting') - setting = BotSetting( - name='news', - data={} - ).save() - - -def down(apps, schema_editor): - BotSetting = apps.get_model('api', 'BotSetting') - BotSetting.objects.get(name='news').delete() - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0050_remove_infractions_active_default_value'), - ] - - operations = [ - migrations.RunPython(up, down) - ] diff --git a/pydis_site/apps/api/migrations/0052_create_news_setting.py b/pydis_site/apps/api/migrations/0052_create_news_setting.py new file mode 100644 index 00000000..b101d19d --- /dev/null +++ b/pydis_site/apps/api/migrations/0052_create_news_setting.py @@ -0,0 +1,25 @@ +from django.db import migrations + + +def up(apps, schema_editor): + BotSetting = apps.get_model('api', 'BotSetting') + setting = BotSetting( + name='news', + data={} + ).save() + + +def down(apps, schema_editor): + BotSetting = apps.get_model('api', 'BotSetting') + BotSetting.objects.get(name='news').delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0051_allow_blank_message_embeds'), + ] + + operations = [ + migrations.RunPython(up, down) + ] diff --git a/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py deleted file mode 100644 index 26b3b954..00000000 --- a/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.2.11 on 2020-05-27 07:17 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0051_create_news_setting'), - ] - - operations = [ - migrations.RemoveField( - model_name='user', - name='avatar_hash', - ), - ] diff --git a/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py new file mode 100644 index 00000000..b51ce1d2 --- /dev/null +++ b/pydis_site/apps/api/migrations/0053_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', '0052_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/0053_user_roles_to_array.py b/pydis_site/apps/api/migrations/0053_user_roles_to_array.py deleted file mode 100644 index 7ff3a548..00000000 --- a/pydis_site/apps/api/migrations/0053_user_roles_to_array.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.2.11 on 2020-06-02 13:42 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0052_remove_user_avatar_hash'), - ] - - operations = [ - migrations.RemoveField( - model_name='user', - name='roles', - ), - migrations.AddField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py new file mode 100644 index 00000000..be9fd948 --- /dev/null +++ b/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.11 on 2020-05-27 07:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0053_offtopicchannelname_used'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='avatar_hash', + ), + ] diff --git a/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py deleted file mode 100644 index 96230015..00000000 --- a/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 2.2.11 on 2020-06-02 20:08 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models -import pydis_site.apps.api.models.bot.user - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0053_user_roles_to_array'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py b/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py deleted file mode 100644 index f2a0e638..00000000 --- a/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-14 20:27 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0051_allow_blank_message_embeds'), - ('api', '0054_user_invalidate_unknown_role'), - ] - - operations = [ - ] diff --git a/pydis_site/apps/api/migrations/0055_reminder_mentions.py b/pydis_site/apps/api/migrations/0055_reminder_mentions.py deleted file mode 100644 index d73b450d..00000000 --- a/pydis_site/apps/api/migrations/0055_reminder_mentions.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.2.14 on 2020-07-15 07:37 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0054_user_invalidate_unknown_role'), - ] - - operations = [ - migrations.AddField( - model_name='reminder', - name='mentions', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0055_user_roles_to_array.py b/pydis_site/apps/api/migrations/0055_user_roles_to_array.py new file mode 100644 index 00000000..e7b4a983 --- /dev/null +++ b/pydis_site/apps/api/migrations/0055_user_roles_to_array.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.11 on 2020-06-02 13:42 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0054_remove_user_avatar_hash'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='roles', + ), + migrations.AddField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py deleted file mode 100644 index 489941c7..00000000 --- a/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-14 20:35 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models -import pydis_site.apps.api.models.bot.user - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0055_merge_20200714_2027'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py new file mode 100644 index 00000000..ab2696aa --- /dev/null +++ b/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.11 on 2020-06-02 20:08 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.user + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0055_user_roles_to_array'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py b/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py deleted file mode 100644 index 47a6d2d4..00000000 --- a/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 2.2.14 on 2020-07-16 07:51 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0055_reminder_mentions'), - ('api', '0056_allow_blank_user_roles'), - ] - - operations = [ - ] diff --git a/pydis_site/apps/api/migrations/0057_reminder_mentions.py b/pydis_site/apps/api/migrations/0057_reminder_mentions.py new file mode 100644 index 00000000..fb829a17 --- /dev/null +++ b/pydis_site/apps/api/migrations/0057_reminder_mentions.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.14 on 2020-07-15 07:37 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0056_user_invalidate_unknown_role'), + ] + + operations = [ + migrations.AddField( + model_name='reminder', + name='mentions', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py new file mode 100644 index 00000000..8f7fddfc --- /dev/null +++ b/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0.8 on 2020-07-14 20:35 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.user + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0057_reminder_mentions'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py deleted file mode 100644 index aecfdad7..00000000 --- a/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-15 11:23 - -from django.db import migrations, models -import pydis_site.apps.api.models.mixins - - -class Migration(migrations.Migration): - dependencies = [ - ('api', '0057_merge_20200716_0751'), - ] - - operations = [ - migrations.CreateModel( - name='FilterList', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('type', models.CharField( - choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), - ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], - help_text='The type of allowlist this is on.', max_length=50)), - ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), - ('content', models.TextField(help_text='The data to add to the allow or denylist.')), - ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), - ], - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - migrations.AddConstraint( - model_name='filterlist', - constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list') - ) - ] diff --git a/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py new file mode 100644 index 00000000..eac5542d --- /dev/null +++ b/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py @@ -0,0 +1,33 @@ +# Generated by Django 3.0.8 on 2020-07-15 11:23 + +from django.db import migrations, models +import pydis_site.apps.api.models.mixins + + +class Migration(migrations.Migration): + dependencies = [ + ('api', '0058_allow_blank_user_roles'), + ] + + operations = [ + migrations.CreateModel( + name='FilterList', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('type', models.CharField( + choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), + ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], + help_text='The type of allowlist this is on.', max_length=50)), + ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), + ('content', models.TextField(help_text='The data to add to the allow or denylist.')), + ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), + ], + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), + ), + migrations.AddConstraint( + model_name='filterlist', + constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list') + ) + ] diff --git a/pydis_site/apps/api/migrations/0059_populate_filterlists.py b/pydis_site/apps/api/migrations/0059_populate_filterlists.py deleted file mode 100644 index 8c550191..00000000 --- a/pydis_site/apps/api/migrations/0059_populate_filterlists.py +++ /dev/null @@ -1,153 +0,0 @@ -from django.db import migrations - -guild_invite_whitelist = [ - ("discord.gg/python", "Python Discord", True), - ("discord.gg/4JJdJKb", "RLBot", True), - ("discord.gg/djPtTRJ", "Kivy", True), - ("discord.gg/QXyegWe", "Pyglet", True), - ("discord.gg/9XsucTT", "Panda3D", True), - ("discord.gg/AP3rq2k", "PyWeek", True), - ("discord.gg/vSPsP9t", "Microsoft Python", True), - ("discord.gg/bRCvFy9", "Discord.js Official", True), - ("discord.gg/9zT7NHP", "Programming Discussions", True), - ("discord.gg/ysd6M4r", "JetBrains Community", True), - ("discord.gg/4xJeCgy", "Raspberry Pie", True), - ("discord.gg/AStb3kZ", "Ren'Py", True), - ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), - ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), - ("discord.gg/jTtgWuy", "Django", True), - ("discord.gg/W9BypZF", "STEM", True), - ("discord.gg/dpy", "discord.py", True), - ("discord.gg/programming", "Programmers Hangout", True), - ("discord.gg/qhGUjGD", "SpeakJS", True), - ("discord.gg/eTbWSZj", "Functional Programming", True), - ("discord.gg/r8yreB6", "PyGame", True), - ("discord.gg/5UBnR3P", "Python Atlanta", True), - ("discord.gg/ccyrDKv", "C#", True), -] - -domain_name_blacklist = [ - ("pornhub.com", None, False), - ("liveleak.com", None, False), - ("grabify.link", None, False), - ("bmwforum.co", None, False), - ("leancoding.co", None, False), - ("spottyfly.com", None, False), - ("stopify.co", None, False), - ("yoütu.be", None, False), - ("discörd.com", None, False), - ("minecräft.com", None, False), - ("freegiftcards.co", None, False), - ("disçordapp.com", None, False), - ("fortnight.space", None, False), - ("fortnitechat.site", None, False), - ("joinmy.site", None, False), - ("curiouscat.club", None, False), - ("catsnthings.fun", None, False), - ("yourtube.site", None, False), - ("youtubeshort.watch", None, False), - ("catsnthing.com", None, False), - ("youtubeshort.pro", None, False), - ("canadianlumberjacks.online", None, False), - ("poweredbydialup.club", None, False), - ("poweredbydialup.online", None, False), - ("poweredbysecurity.org", None, False), - ("poweredbysecurity.online", None, False), - ("ssteam.site", None, False), - ("steamwalletgift.com", None, False), - ("discord.gift", None, False), - ("lmgtfy.com", None, False), -] - -filter_token_blacklist = [ - ("\bgoo+ks*\b", None, False), - ("\bky+s+\b", None, False), - ("\bki+ke+s*\b", None, False), - ("\bbeaner+s?\b", None, False), - ("\bcoo+ns*\b", None, False), - ("\bnig+lets*\b", None, False), - ("\bslant-eyes*\b", None, False), - ("\btowe?l-?head+s*\b", None, False), - ("\bchi*n+k+s*\b", None, False), - ("\bspick*s*\b", None, False), - ("\bkill* +(?:yo)?urself+\b", None, False), - ("\bjew+s*\b", None, False), - ("\bsuicide\b", None, False), - ("\brape\b", None, False), - ("\b(re+)tar+(d+|t+)(ed)?\b", None, False), - ("\bta+r+d+\b", None, False), - ("\bcunts*\b", None, False), - ("\btrann*y\b", None, False), - ("\bshemale\b", None, False), - ("fa+g+s*", None, False), - ("卐", None, False), - ("卍", None, False), - ("࿖", None, False), - ("࿕", None, False), - ("࿘", None, False), - ("࿗", None, False), - ("cuck(?!oo+)", None, False), - ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), - ("fag+o+t+s*", None, False), -] - -file_format_whitelist = [ - (".3gp", None, True), - (".3g2", None, True), - (".avi", None, True), - (".bmp", None, True), - (".gif", None, True), - (".h264", None, True), - (".jpg", None, True), - (".jpeg", None, True), - (".m4v", None, True), - (".mkv", None, True), - (".mov", None, True), - (".mp4", None, True), - (".mpeg", None, True), - (".mpg", None, True), - (".png", None, True), - (".tiff", None, True), - (".wmv", None, True), - (".svg", None, True), - (".psd", "Photoshop", True), - (".ai", "Illustrator", True), - (".aep", "After Effects", True), - (".xcf", "GIMP", True), - (".mp3", None, True), - (".wav", None, True), - (".ogg", None, True), - (".webm", None, True), - (".webp", None, True), -] - -populate_data = { - "FILTER_TOKEN": filter_token_blacklist, - "DOMAIN_NAME": domain_name_blacklist, - "FILE_FORMAT": file_format_whitelist, - "GUILD_INVITE": guild_invite_whitelist, -} - - -class Migration(migrations.Migration): - dependencies = [("api", "0058_create_new_filterlist_model")] - - def populate_filterlists(app, _): - FilterList = app.get_model("api", "FilterList") - - for filterlist_type, metadata in populate_data.items(): - for content, comment, allowed in metadata: - FilterList.objects.create( - type=filterlist_type, - allowed=allowed, - content=content, - comment=comment, - ) - - def clear_filterlists(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.all().delete() - - operations = [ - migrations.RunPython(populate_filterlists, clear_filterlists) - ] diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists.py b/pydis_site/apps/api/migrations/0060_populate_filterlists.py new file mode 100644 index 00000000..35fde95a --- /dev/null +++ b/pydis_site/apps/api/migrations/0060_populate_filterlists.py @@ -0,0 +1,153 @@ +from django.db import migrations + +guild_invite_whitelist = [ + ("discord.gg/python", "Python Discord", True), + ("discord.gg/4JJdJKb", "RLBot", True), + ("discord.gg/djPtTRJ", "Kivy", True), + ("discord.gg/QXyegWe", "Pyglet", True), + ("discord.gg/9XsucTT", "Panda3D", True), + ("discord.gg/AP3rq2k", "PyWeek", True), + ("discord.gg/vSPsP9t", "Microsoft Python", True), + ("discord.gg/bRCvFy9", "Discord.js Official", True), + ("discord.gg/9zT7NHP", "Programming Discussions", True), + ("discord.gg/ysd6M4r", "JetBrains Community", True), + ("discord.gg/4xJeCgy", "Raspberry Pie", True), + ("discord.gg/AStb3kZ", "Ren'Py", True), + ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), + ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), + ("discord.gg/jTtgWuy", "Django", True), + ("discord.gg/W9BypZF", "STEM", True), + ("discord.gg/dpy", "discord.py", True), + ("discord.gg/programming", "Programmers Hangout", True), + ("discord.gg/qhGUjGD", "SpeakJS", True), + ("discord.gg/eTbWSZj", "Functional Programming", True), + ("discord.gg/r8yreB6", "PyGame", True), + ("discord.gg/5UBnR3P", "Python Atlanta", True), + ("discord.gg/ccyrDKv", "C#", True), +] + +domain_name_blacklist = [ + ("pornhub.com", None, False), + ("liveleak.com", None, False), + ("grabify.link", None, False), + ("bmwforum.co", None, False), + ("leancoding.co", None, False), + ("spottyfly.com", None, False), + ("stopify.co", None, False), + ("yoütu.be", None, False), + ("discörd.com", None, False), + ("minecräft.com", None, False), + ("freegiftcards.co", None, False), + ("disçordapp.com", None, False), + ("fortnight.space", None, False), + ("fortnitechat.site", None, False), + ("joinmy.site", None, False), + ("curiouscat.club", None, False), + ("catsnthings.fun", None, False), + ("yourtube.site", None, False), + ("youtubeshort.watch", None, False), + ("catsnthing.com", None, False), + ("youtubeshort.pro", None, False), + ("canadianlumberjacks.online", None, False), + ("poweredbydialup.club", None, False), + ("poweredbydialup.online", None, False), + ("poweredbysecurity.org", None, False), + ("poweredbysecurity.online", None, False), + ("ssteam.site", None, False), + ("steamwalletgift.com", None, False), + ("discord.gift", None, False), + ("lmgtfy.com", None, False), +] + +filter_token_blacklist = [ + ("\bgoo+ks*\b", None, False), + ("\bky+s+\b", None, False), + ("\bki+ke+s*\b", None, False), + ("\bbeaner+s?\b", None, False), + ("\bcoo+ns*\b", None, False), + ("\bnig+lets*\b", None, False), + ("\bslant-eyes*\b", None, False), + ("\btowe?l-?head+s*\b", None, False), + ("\bchi*n+k+s*\b", None, False), + ("\bspick*s*\b", None, False), + ("\bkill* +(?:yo)?urself+\b", None, False), + ("\bjew+s*\b", None, False), + ("\bsuicide\b", None, False), + ("\brape\b", None, False), + ("\b(re+)tar+(d+|t+)(ed)?\b", None, False), + ("\bta+r+d+\b", None, False), + ("\bcunts*\b", None, False), + ("\btrann*y\b", None, False), + ("\bshemale\b", None, False), + ("fa+g+s*", None, False), + ("卐", None, False), + ("卍", None, False), + ("࿖", None, False), + ("࿕", None, False), + ("࿘", None, False), + ("࿗", None, False), + ("cuck(?!oo+)", None, False), + ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), + ("fag+o+t+s*", None, False), +] + +file_format_whitelist = [ + (".3gp", None, True), + (".3g2", None, True), + (".avi", None, True), + (".bmp", None, True), + (".gif", None, True), + (".h264", None, True), + (".jpg", None, True), + (".jpeg", None, True), + (".m4v", None, True), + (".mkv", None, True), + (".mov", None, True), + (".mp4", None, True), + (".mpeg", None, True), + (".mpg", None, True), + (".png", None, True), + (".tiff", None, True), + (".wmv", None, True), + (".svg", None, True), + (".psd", "Photoshop", True), + (".ai", "Illustrator", True), + (".aep", "After Effects", True), + (".xcf", "GIMP", True), + (".mp3", None, True), + (".wav", None, True), + (".ogg", None, True), + (".webm", None, True), + (".webp", None, True), +] + +populate_data = { + "FILTER_TOKEN": filter_token_blacklist, + "DOMAIN_NAME": domain_name_blacklist, + "FILE_FORMAT": file_format_whitelist, + "GUILD_INVITE": guild_invite_whitelist, +} + + +class Migration(migrations.Migration): + dependencies = [("api", "0059_create_new_filterlist_model")] + + def populate_filterlists(app, _): + FilterList = app.get_model("api", "FilterList") + + for filterlist_type, metadata in populate_data.items(): + for content, comment, allowed in metadata: + FilterList.objects.create( + type=filterlist_type, + allowed=allowed, + content=content, + comment=comment, + ) + + def clear_filterlists(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.all().delete() + + operations = [ + migrations.RunPython(populate_filterlists, clear_filterlists) + ] diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py deleted file mode 100644 index 53846f02..00000000 --- a/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py +++ /dev/null @@ -1,85 +0,0 @@ -from django.db import migrations - -bad_guild_invite_whitelist = [ - ("discord.gg/python", "Python Discord", True), - ("discord.gg/4JJdJKb", "RLBot", True), - ("discord.gg/djPtTRJ", "Kivy", True), - ("discord.gg/QXyegWe", "Pyglet", True), - ("discord.gg/9XsucTT", "Panda3D", True), - ("discord.gg/AP3rq2k", "PyWeek", True), - ("discord.gg/vSPsP9t", "Microsoft Python", True), - ("discord.gg/bRCvFy9", "Discord.js Official", True), - ("discord.gg/9zT7NHP", "Programming Discussions", True), - ("discord.gg/ysd6M4r", "JetBrains Community", True), - ("discord.gg/4xJeCgy", "Raspberry Pie", True), - ("discord.gg/AStb3kZ", "Ren'Py", True), - ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), - ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), - ("discord.gg/jTtgWuy", "Django", True), - ("discord.gg/W9BypZF", "STEM", True), - ("discord.gg/dpy", "discord.py", True), - ("discord.gg/programming", "Programmers Hangout", True), - ("discord.gg/qhGUjGD", "SpeakJS", True), - ("discord.gg/eTbWSZj", "Functional Programming", True), - ("discord.gg/r8yreB6", "PyGame", True), - ("discord.gg/5UBnR3P", "Python Atlanta", True), - ("discord.gg/ccyrDKv", "C#", True), -] - -guild_invite_whitelist = [ - ("267624335836053506", "Python Discord", True), - ("348658686962696195", "RLBot", True), - ("423249981340778496", "Kivy", True), - ("438622377094414346", "Pyglet", True), - ("524691714909274162", "Panda3D", True), - ("666560367173828639", "PyWeek", True), - ("702724176489873509", "Microsoft Python", True), - ("222078108977594368", "Discord.js Official", True), - ("238666723824238602", "Programming Discussions", True), - ("433980600391696384", "JetBrains Community", True), - ("204621105720328193", "Raspberry Pie", True), - ("286633898581164032", "Ren'Py", True), - ("440186186024222721", "Python Discord: Emojis 1", True), - ("578587418123304970", "Python Discord: Emojis 2", True), - ("159039020565790721", "Django", True), - ("273944235143593984", "STEM", True), - ("336642139381301249", "discord.py", True), - ("244230771232079873", "Programmers Hangout", True), - ("239433591950540801", "SpeakJS", True), - ("280033776820813825", "Functional Programming", True), - ("349505959032389632", "PyGame", True), - ("488751051629920277", "Python Atlanta", True), - ("143867839282020352", "C#", True), -] - - -class Migration(migrations.Migration): - dependencies = [("api", "0059_populate_filterlists")] - - def fix_filterlist(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.filter(type="GUILD_INVITE").delete() # Clear out the stuff added in 0059. - - for content, comment, allowed in guild_invite_whitelist: - FilterList.objects.create( - type="GUILD_INVITE", - allowed=allowed, - content=content, - comment=comment, - ) - - def restore_bad_filterlist(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.filter(type="GUILD_INVITE").delete() - - for content, comment, allowed in bad_guild_invite_whitelist: - FilterList.objects.create( - type="GUILD_INVITE", - allowed=allowed, - content=content, - comment=comment, - ) - - operations = [ - migrations.RunPython(fix_filterlist, restore_bad_filterlist) - ] diff --git a/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py new file mode 100644 index 00000000..eaaafb38 --- /dev/null +++ b/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py @@ -0,0 +1,85 @@ +from django.db import migrations + +bad_guild_invite_whitelist = [ + ("discord.gg/python", "Python Discord", True), + ("discord.gg/4JJdJKb", "RLBot", True), + ("discord.gg/djPtTRJ", "Kivy", True), + ("discord.gg/QXyegWe", "Pyglet", True), + ("discord.gg/9XsucTT", "Panda3D", True), + ("discord.gg/AP3rq2k", "PyWeek", True), + ("discord.gg/vSPsP9t", "Microsoft Python", True), + ("discord.gg/bRCvFy9", "Discord.js Official", True), + ("discord.gg/9zT7NHP", "Programming Discussions", True), + ("discord.gg/ysd6M4r", "JetBrains Community", True), + ("discord.gg/4xJeCgy", "Raspberry Pie", True), + ("discord.gg/AStb3kZ", "Ren'Py", True), + ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), + ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), + ("discord.gg/jTtgWuy", "Django", True), + ("discord.gg/W9BypZF", "STEM", True), + ("discord.gg/dpy", "discord.py", True), + ("discord.gg/programming", "Programmers Hangout", True), + ("discord.gg/qhGUjGD", "SpeakJS", True), + ("discord.gg/eTbWSZj", "Functional Programming", True), + ("discord.gg/r8yreB6", "PyGame", True), + ("discord.gg/5UBnR3P", "Python Atlanta", True), + ("discord.gg/ccyrDKv", "C#", True), +] + +guild_invite_whitelist = [ + ("267624335836053506", "Python Discord", True), + ("348658686962696195", "RLBot", True), + ("423249981340778496", "Kivy", True), + ("438622377094414346", "Pyglet", True), + ("524691714909274162", "Panda3D", True), + ("666560367173828639", "PyWeek", True), + ("702724176489873509", "Microsoft Python", True), + ("222078108977594368", "Discord.js Official", True), + ("238666723824238602", "Programming Discussions", True), + ("433980600391696384", "JetBrains Community", True), + ("204621105720328193", "Raspberry Pie", True), + ("286633898581164032", "Ren'Py", True), + ("440186186024222721", "Python Discord: Emojis 1", True), + ("578587418123304970", "Python Discord: Emojis 2", True), + ("159039020565790721", "Django", True), + ("273944235143593984", "STEM", True), + ("336642139381301249", "discord.py", True), + ("244230771232079873", "Programmers Hangout", True), + ("239433591950540801", "SpeakJS", True), + ("280033776820813825", "Functional Programming", True), + ("349505959032389632", "PyGame", True), + ("488751051629920277", "Python Atlanta", True), + ("143867839282020352", "C#", True), +] + + +class Migration(migrations.Migration): + dependencies = [("api", "0060_populate_filterlists")] + + def fix_filterlist(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.filter(type="GUILD_INVITE").delete() # Clear out the stuff added in 0059. + + for content, comment, allowed in guild_invite_whitelist: + FilterList.objects.create( + type="GUILD_INVITE", + allowed=allowed, + content=content, + comment=comment, + ) + + def restore_bad_filterlist(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.filter(type="GUILD_INVITE").delete() + + for content, comment, allowed in bad_guild_invite_whitelist: + FilterList.objects.create( + type="GUILD_INVITE", + allowed=allowed, + content=content, + comment=comment, + ) + + operations = [ + migrations.RunPython(fix_filterlist, restore_bad_filterlist) + ] 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/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/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) -- cgit v1.2.3 From 49e7243f6527140dbdb29a2f1dd994839f248dba Mon Sep 17 00:00:00 2001 From: Eivind Teig Date: Fri, 11 Sep 2020 23:43:06 +0200 Subject: Allow blank/null input to the nomination reason. We are lowering the threshold for nomination. By allowing the users to make a nomination without a reason might make this feature more attractive amongst members of staff. --- .../0063_Allow_blank_or_null_for_nomination_reason.py | 18 ++++++++++++++++++ pydis_site/apps/api/models/bot/nomination.py | 4 +++- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 pydis_site/apps/api/migrations/0063_Allow_blank_or_null_for_nomination_reason.py (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/migrations/0063_Allow_blank_or_null_for_nomination_reason.py b/pydis_site/apps/api/migrations/0063_Allow_blank_or_null_for_nomination_reason.py new file mode 100644 index 00000000..9eb05eaa --- /dev/null +++ b/pydis_site/apps/api/migrations/0063_Allow_blank_or_null_for_nomination_reason.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.9 on 2020-09-11 21:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0062_merge_20200901_1459'), + ] + + operations = [ + migrations.AlterField( + model_name='nomination', + name='reason', + field=models.TextField(blank=True, help_text='Why this user was nominated.', null=True), + ), + ] diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py index 21e34e87..183b22d5 100644 --- a/pydis_site/apps/api/models/bot/nomination.py +++ b/pydis_site/apps/api/models/bot/nomination.py @@ -18,7 +18,9 @@ class Nomination(ModelReprMixin, models.Model): related_name='nomination_set' ) reason = models.TextField( - help_text="Why this user was nominated." + help_text="Why this user was nominated.", + null=True, + blank=True ) user = models.ForeignKey( User, -- cgit v1.2.3 From a311ad08003203a5aa38bf49fec69d6d42f74439 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 18 Sep 2020 11:48:54 +1000 Subject: Update UserAdmin to use new role values, pretty colours. --- pydis_site/apps/api/admin.py | 78 ++++++++++++++++++++-------------- pydis_site/apps/api/models/bot/user.py | 4 ++ 2 files changed, 50 insertions(+), 32 deletions(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index 6116fbf8..6101f88c 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -1,9 +1,10 @@ +from __future__ import annotations + import json -from typing import Optional, Tuple +from typing import Optional from django import urls from django.contrib import admin -from django.db.models.query import QuerySet from django.http import HttpRequest from django.utils.html import format_html @@ -249,45 +250,58 @@ class RoleAdmin(admin.ModelAdmin): permissions_with_calc_link.short_description = "Permissions" -class StaffRolesFilter(admin.SimpleListFilter): - """Filter options for Staff Roles.""" +class UserTopRoleFilter(admin.SimpleListFilter): + """List Filter for User list Admin page.""" - title = "Staff Role" - parameter_name = "staff_role" + title = "Role" + parameter_name = "role" - @staticmethod - def lookups(*_) -> Tuple[Tuple[str, str], ...]: - """Available filter options.""" - return ( - ("Owners", "Owners"), - ("Admins", "Admins"), - ("Moderators", "Moderators"), - ("Core Developers", "Core Developers"), - ("Helpers", "Helpers"), - ) + def lookups(self, request, model_admin: UserAdmin): + """Selectable values for viewer to filter by.""" + roles = Role.objects.all() + return ((r.name, r.name) for r in roles) - def queryset(self, request: HttpRequest, queryset: QuerySet) -> Optional[QuerySet]: - """Returned data filter based on selected option.""" - value = self.value() - if value: - return queryset.filter(roles__name=value) + def queryset(self, request, queryset): + if not self.value(): + return + role = Role.objects.get(name=self.value()) + return queryset.filter(roles__contains=[role.id]) @admin.register(User) class UserAdmin(admin.ModelAdmin): """Admin formatting for the User model.""" - search_fields = ("name", "id", "roles__name", "roles__id") - list_filter = ("in_guild", StaffRolesFilter) - exclude = ("name", "discriminator") - readonly_fields = ( - "__str__", - "id", - "avatar_hash", - "top_role", - "roles", - "in_guild", - ) + def top_role_coloured(self, obj: User): + """Returns the top role of the user with html style matching role colour.""" + return format_html( + f'{obj.top_role.name}' + ) + + top_role_coloured.short_description = "Top Role" + + def all_roles_coloured(self, obj: User): + """Returns all user roles with html style matching role colours.""" + roles = Role.objects.filter(id__in=obj.roles) + return format_html( + "
".join( + f'{r.name}' for r in roles + ) + ) + + all_roles_coloured.short_description = "All Roles" + + search_fields = ("name", "id", "roles") + list_filter = (UserTopRoleFilter, "in_guild") + list_display = ("username", "top_role_coloured", "in_guild") + fields = ("username", "id", "in_guild", "all_roles_coloured") + sortable_by = ("username",) + + def has_add_permission(self, request): + return False + + def has_change_permission(self, request, obj=None): + return False admin.site.register(BotSetting) diff --git a/pydis_site/apps/api/models/bot/user.py b/pydis_site/apps/api/models/bot/user.py index cd2d58b9..70a30449 100644 --- a/pydis_site/apps/api/models/bot/user.py +++ b/pydis_site/apps/api/models/bot/user.py @@ -75,3 +75,7 @@ class User(ModelReprMixin, models.Model): if not roles: return Role.objects.get(name="Developers") return max(roles) + + @property + def username(self): + return f"{self.name}#{self.discriminator:04d}" -- cgit v1.2.3 From dfbce19d39b0b6243865e682b8e6d86b8a028a71 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 18 Sep 2020 12:00:13 +1000 Subject: Add verbose names for user fields that need capitalisation fixes. --- pydis_site/apps/api/models/bot/user.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/bot/user.py b/pydis_site/apps/api/models/bot/user.py index 70a30449..a8604001 100644 --- a/pydis_site/apps/api/models/bot/user.py +++ b/pydis_site/apps/api/models/bot/user.py @@ -26,11 +26,12 @@ class User(ModelReprMixin, models.Model): message="User IDs cannot be negative." ), ), + verbose_name="ID", help_text="The ID of this user, taken from Discord." ) name = models.CharField( max_length=32, - help_text="The username, taken from Discord." + help_text="The username, taken from Discord.", ) discriminator = models.PositiveSmallIntegerField( validators=( @@ -57,7 +58,8 @@ class User(ModelReprMixin, models.Model): ) in_guild = models.BooleanField( default=True, - help_text="Whether this user is in our server." + help_text="Whether this user is in our server.", + verbose_name="In Guild" ) def __str__(self): -- cgit v1.2.3 From 77c9e3ffce7992706202cf20ae3addf42c4dbf6c Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 18 Sep 2020 12:40:24 +1000 Subject: Add return types and docstrings for new user admin changes. --- pydis_site/apps/api/admin.py | 29 ++++++++++++++++++----------- pydis_site/apps/api/models/bot/user.py | 11 ++++++++--- 2 files changed, 26 insertions(+), 14 deletions(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index 6101f88c..d77ae620 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -1,12 +1,13 @@ from __future__ import annotations import json -from typing import Optional +from typing import Iterable, Optional, Tuple from django import urls from django.contrib import admin +from django.db.models import QuerySet from django.http import HttpRequest -from django.utils.html import format_html +from django.utils.html import SafeString, format_html from .models import ( BotSetting, @@ -256,12 +257,13 @@ class UserTopRoleFilter(admin.SimpleListFilter): title = "Role" parameter_name = "role" - def lookups(self, request, model_admin: UserAdmin): + def lookups(self, request: HttpRequest, model_admin: UserAdmin) -> Iterable[Tuple[str, str]]: """Selectable values for viewer to filter by.""" roles = Role.objects.all() return ((r.name, r.name) for r in roles) - def queryset(self, request, queryset): + def queryset(self, request: HttpRequest, queryset: QuerySet) -> Optional[QuerySet]: + """Query to filter the list of Users against.""" if not self.value(): return role = Role.objects.get(name=self.value()) @@ -272,20 +274,23 @@ class UserTopRoleFilter(admin.SimpleListFilter): class UserAdmin(admin.ModelAdmin): """Admin formatting for the User model.""" - def top_role_coloured(self, obj: User): + def top_role_coloured(self, user: User) -> SafeString: """Returns the top role of the user with html style matching role colour.""" return format_html( - f'{obj.top_role.name}' + '{1}', + user.top_role.colour, + user.top_role.name ) top_role_coloured.short_description = "Top Role" - def all_roles_coloured(self, obj: User): + def all_roles_coloured(self, user: User) -> SafeString: """Returns all user roles with html style matching role colours.""" - roles = Role.objects.filter(id__in=obj.roles) + roles = Role.objects.filter(id__in=user.roles) return format_html( "
".join( - f'{r.name}' for r in roles + f'{r.name}' + for r in roles ) ) @@ -297,10 +302,12 @@ class UserAdmin(admin.ModelAdmin): fields = ("username", "id", "in_guild", "all_roles_coloured") sortable_by = ("username",) - def has_add_permission(self, request): + def has_add_permission(self, *args) -> bool: + """Prevent adding from django admin.""" return False - def has_change_permission(self, request, obj=None): + def has_change_permission(self, *args) -> bool: + """Prevent editing from django admin.""" return False diff --git a/pydis_site/apps/api/models/bot/user.py b/pydis_site/apps/api/models/bot/user.py index a8604001..afc5ba1e 100644 --- a/pydis_site/apps/api/models/bot/user.py +++ b/pydis_site/apps/api/models/bot/user.py @@ -64,7 +64,7 @@ class User(ModelReprMixin, models.Model): def __str__(self): """Returns the name and discriminator for the current user, for display purposes.""" - return f"{self.name}#{self.discriminator:0>4}" + return f"{self.name}#{self.discriminator:04d}" @property def top_role(self) -> Role: @@ -79,5 +79,10 @@ class User(ModelReprMixin, models.Model): return max(roles) @property - def username(self): - return f"{self.name}#{self.discriminator:04d}" + def username(self) -> str: + """ + Returns the display version with name and discriminator as a standard attribute. + + For usability in read-only fields such as Django Admin. + """ + return str(self) -- cgit v1.2.3 From bbecf18704837f087e8254b657adf1c5e859dff9 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 18 Sep 2020 12:47:32 +1000 Subject: Update Role ModelAdmin to past changes, cleanup formatting. --- pydis_site/apps/api/admin.py | 45 ++++++++++++++++++++++------------ pydis_site/apps/api/models/bot/role.py | 3 ++- 2 files changed, 31 insertions(+), 17 deletions(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index 15a1ec78..d32b4911 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -222,34 +222,47 @@ class OffTopicChannelNameAdmin(admin.ModelAdmin): class RoleAdmin(admin.ModelAdmin): """Admin formatting for the Role model.""" - exclude = ("permissions", "colour") - readonly_fields = ( - "name", - "id", - "colour_with_preview", - "permissions_with_calc_link", - "position" - ) - search_fields = ("name", "id") + def coloured_name(self, role: Role) -> SafeString: + """Role name with html style colouring.""" + return format_html( + '{1}', + f"#{role.colour:06X}", + role.name + ) + + coloured_name.short_description = "Name" - def colour_with_preview(self, instance: Role) -> str: + def colour_with_preview(self, role: Role) -> SafeString: """Show colour value in both int and hex, in bolded and coloured style.""" return format_html( - "{1} / #{0}", - f"{instance.colour:06x}", - instance.colour + "{0} ({1})", + f"#{role.colour:06x}", + role.colour ) - def permissions_with_calc_link(self, instance: Role) -> str: + colour_with_preview.short_description = "Colour" + + def permissions_with_calc_link(self, role: Role) -> SafeString: """Show permissions with link to API permissions calculator page.""" return format_html( "{0}", - instance.permissions + role.permissions ) - colour_with_preview.short_description = "Colour" permissions_with_calc_link.short_description = "Permissions" + search_fields = ("name", "id") + list_display = ("coloured_name",) + fields = ("id", "name", "colour_with_preview", "permissions_with_calc_link", "position") + + def has_add_permission(self, *args) -> bool: + """Prevent adding from django admin.""" + return False + + def has_change_permission(self, *args) -> bool: + """Prevent editing from django admin.""" + return False + class UserTopRoleFilter(admin.SimpleListFilter): """List Filter for User list Admin page.""" diff --git a/pydis_site/apps/api/models/bot/role.py b/pydis_site/apps/api/models/bot/role.py index b23fc5f4..cfadfec4 100644 --- a/pydis_site/apps/api/models/bot/role.py +++ b/pydis_site/apps/api/models/bot/role.py @@ -22,7 +22,8 @@ class Role(ModelReprMixin, models.Model): message="Role IDs cannot be negative." ), ), - help_text="The role ID, taken from Discord." + help_text="The role ID, taken from Discord.", + verbose_name="ID" ) name = models.CharField( max_length=100, -- cgit v1.2.3 From 8ed8b9ad62fd83e93d8dd5a136b7433f7240c4c2 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 18 Sep 2020 13:01:49 +1000 Subject: Add OffensiveMessage Admin model. --- pydis_site/apps/api/admin.py | 25 +++++++++++++++++++++- .../apps/api/models/bot/offensive_message.py | 9 +++++--- 2 files changed, 30 insertions(+), 4 deletions(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index d32b4911..59cab78d 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -218,6 +218,30 @@ class OffTopicChannelNameAdmin(admin.ModelAdmin): search_fields = ("name",) +@admin.register(OffensiveMessage) +class OffensiveMessageAdmin(admin.ModelAdmin): + """Admin formatting for the OffensiveMessage model.""" + + def message_jumplink(self, message: OffensiveMessage) -> SafeString: + """Message ID hyperlinked to the direct discord jumplink.""" + return format_html( + '{1}', + message.channel_id, + message.id + ) + + message_jumplink.short_description = "Message ID" + + search_fields = ("id", "channel_id") + list_display = ("id", "channel_id", "delete_date") + fields = ("message_jumplink", "channel_id", "delete_date") + readonly_fields = ("message_jumplink", "channel_id") + + def has_add_permission(self, *args) -> bool: + """Prevent adding from django admin.""" + return False + + @admin.register(Role) class RoleAdmin(admin.ModelAdmin): """Admin formatting for the Role model.""" @@ -326,4 +350,3 @@ class UserAdmin(admin.ModelAdmin): admin.site.register(BotSetting) admin.site.register(DocumentationLink) -admin.site.register(OffensiveMessage) diff --git a/pydis_site/apps/api/models/bot/offensive_message.py b/pydis_site/apps/api/models/bot/offensive_message.py index 6c0e5ffb..74dab59b 100644 --- a/pydis_site/apps/api/models/bot/offensive_message.py +++ b/pydis_site/apps/api/models/bot/offensive_message.py @@ -24,7 +24,8 @@ class OffensiveMessage(ModelReprMixin, models.Model): limit_value=0, message="Message IDs cannot be negative." ), - ) + ), + verbose_name="Message ID" ) channel_id = models.BigIntegerField( help_text=( @@ -36,11 +37,13 @@ class OffensiveMessage(ModelReprMixin, models.Model): limit_value=0, message="Channel IDs cannot be negative." ), - ) + ), + verbose_name="Channel ID" ) delete_date = models.DateTimeField( help_text="The date on which the message will be auto-deleted.", - validators=(future_date_validator,) + validators=(future_date_validator,), + verbose_name="To Be Deleted" ) def __str__(self): -- cgit v1.2.3 From 5c2e750b6ff19248aaafbb0a1c12f8c7523b7273 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 18 Sep 2020 14:59:30 +1000 Subject: Update DeletedMessage and LogEntry Admin models, add verbose names for Message --- pydis_site/apps/api/admin.py | 167 ++++++++++++++++-------------- pydis_site/apps/api/models/bot/message.py | 6 +- 2 files changed, 95 insertions(+), 78 deletions(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index a85b4cac..ca97512f 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -23,34 +23,77 @@ from .models import ( User ) +admin.site.site_header = "Python Discord | Administration" +admin.site.site_title = "Python Discord" + + +@admin.register(Infraction) +class InfractionAdmin(admin.ModelAdmin): + """Admin formatting for the Infraction model.""" + + fields = ( + "user", + "actor", + "type", + "reason", + "inserted_at", + "expires_at", + "active", + "hidden" + ) + readonly_fields = ( + "user", + "actor", + "type", + "inserted_at" + ) + list_display = ( + "type", + "user", + "actor", + "inserted_at", + "expires_at", + "reason", + "active", + ) + search_fields = ( + "id", + "user__name", + "user__id", + "actor__name", + "actor__id", + "reason", + "type" + ) + list_filter = ( + "type", + "hidden", + "active" + ) + @admin.register(LogEntry) class LogEntryAdmin(admin.ModelAdmin): """Allows viewing logs in the Django Admin without allowing edits.""" actions = None - list_display = ('timestamp', 'application', 'level', 'message') + list_display = ('timestamp', 'level', 'message') fieldsets = ( ('Overview', {'fields': ('timestamp', 'application', 'logger_name')}), ('Metadata', {'fields': ('level', 'module', 'line')}), ('Contents', {'fields': ('message',)}) ) - list_filter = ('application', 'level', 'timestamp') + list_filter = ('level', 'timestamp') search_fields = ('message',) - readonly_fields = ( - 'application', - 'logger_name', - 'timestamp', - 'level', - 'module', - 'line', - 'message' - ) def has_add_permission(self, request: HttpRequest) -> bool: """Deny manual LogEntry creation.""" return False + def has_change_permission(self, *args) -> bool: + """Prevent editing from django admin.""" + return False + def has_delete_permission(self, request: HttpRequest, obj: Optional[LogEntry] = None) -> bool: """Deny LogEntry deletion.""" return False @@ -60,7 +103,7 @@ class LogEntryAdmin(admin.ModelAdmin): class DeletedMessageAdmin(admin.ModelAdmin): """Admin formatting for the DeletedMessage model.""" - readonly_fields = ( + fields = ( "id", "author", "channel_id", @@ -81,96 +124,68 @@ class DeletedMessageAdmin(admin.ModelAdmin): "deletion_context__actor__id" ) - @staticmethod - def embed_data(instance: DeletedMessage) -> Optional[str]: + def embed_data(self, message: DeletedMessage) -> Optional[str]: """Format embed data in a code block for better readability.""" - if instance.embeds: + if message.embeds: return format_html( "
"
                 "{0}
", - json.dumps(instance.embeds, indent=4) + json.dumps(message.embeds, indent=4) ) + embed_data.short_description = "Embeds" + @staticmethod - def context(instance: DeletedMessage) -> str: + def context(message: DeletedMessage) -> str: """Provide full context info with a link through to context admin view.""" link = urls.reverse( "admin:api_messagedeletioncontext_change", - args=[instance.deletion_context.id] + args=[message.deletion_context.id] ) details = ( - f"Deleted by {instance.deletion_context.actor} at " - f"{instance.deletion_context.creation}" + f"Deleted by {message.deletion_context.actor} at " + f"{message.deletion_context.creation}" ) return format_html("{1}", link, details) @staticmethod - def view_full_log(instance: DeletedMessage) -> str: + def view_full_log(message: DeletedMessage) -> str: """Provide a link to the message logs for the relevant context.""" return format_html( "Click to view full context log", - instance.deletion_context.log_url + message.deletion_context.log_url ) + def has_add_permission(self, *args) -> bool: + """Prevent adding from django admin.""" + return False + + def has_change_permission(self, *args) -> bool: + """Prevent editing from django admin.""" + return False + + +class DeletedMessageInline(admin.TabularInline): + """Tabular Inline Admin model for Deleted Message to be viewed within Context.""" + + model = DeletedMessage + @admin.register(MessageDeletionContext) class MessageDeletionContextAdmin(admin.ModelAdmin): """Admin formatting for the MessageDeletionContext model.""" - readonly_fields = ("actor", "creation", "message_log") + fields = ("actor", "creation") + list_display = ("id", "creation", "actor") + inlines = (DeletedMessageInline,) - @staticmethod - def message_log(instance: MessageDeletionContext) -> str: - """Provide a formatted link to the message logs for the context.""" - return format_html( - "Click to see deleted message log", - instance.log_url - ) - - -@admin.register(Infraction) -class InfractionAdmin(admin.ModelAdmin): - """Admin formatting for the Infraction model.""" + def has_add_permission(self, *args) -> bool: + """Prevent adding from django admin.""" + return False - fields = ( - "user", - "actor", - "type", - "reason", - "inserted_at", - "expires_at", - "active", - "hidden" - ) - readonly_fields = ( - "user", - "actor", - "type", - "inserted_at" - ) - list_display = ( - "type", - "user", - "actor", - "inserted_at", - "expires_at", - "reason", - "active", - ) - search_fields = ( - "id", - "user__name", - "user__id", - "actor__name", - "actor__id", - "reason", - "type" - ) - list_filter = ( - "type", - "hidden", - "active" - ) + def has_change_permission(self, *args) -> bool: + """Prevent editing from django admin.""" + return False class NominationActorFilter(admin.SimpleListFilter): @@ -179,7 +194,7 @@ class NominationActorFilter(admin.SimpleListFilter): title = "Actor" parameter_name = "actor" - def lookups(self, request: HttpRequest, model_admin: NominationAdmin) -> Iterable[Tuple[int, str]]: + def lookups(self, request: HttpRequest, model: NominationAdmin) -> Iterable[Tuple[int, str]]: """Selectable values for viewer to filter by.""" actor_ids = Nomination.objects.order_by().values_list("actor").distinct() actors = User.objects.filter(id__in=actor_ids) @@ -322,7 +337,7 @@ class UserTopRoleFilter(admin.SimpleListFilter): title = "Role" parameter_name = "role" - def lookups(self, request: HttpRequest, model_admin: UserAdmin) -> Iterable[Tuple[str, str]]: + def lookups(self, request: HttpRequest, model: UserAdmin) -> Iterable[Tuple[str, str]]: """Selectable values for viewer to filter by.""" roles = Role.objects.all() return ((r.name, r.name) for r in roles) diff --git a/pydis_site/apps/api/models/bot/message.py b/pydis_site/apps/api/models/bot/message.py index f6ae55a5..ff06de21 100644 --- a/pydis_site/apps/api/models/bot/message.py +++ b/pydis_site/apps/api/models/bot/message.py @@ -21,7 +21,8 @@ class Message(ModelReprMixin, models.Model): limit_value=0, message="Message IDs cannot be negative." ), - ) + ), + verbose_name="ID" ) author = models.ForeignKey( User, @@ -38,7 +39,8 @@ class Message(ModelReprMixin, models.Model): limit_value=0, message="Channel IDs cannot be negative." ), - ) + ), + verbose_name="Channel ID" ) content = models.CharField( max_length=2_000, -- cgit v1.2.3 From 715dd46aa68358cdd2abee07b018ee08e54da8d9 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sun, 20 Sep 2020 05:02:48 +1000 Subject: Allow Nomination end_reason to have a blank value for validation. Without `blank=True`, admin page editable forms could not be saved if no content was in the end_reason input. --- pydis_site/apps/api/models/bot/nomination.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py index 54f56c98..11b9e36e 100644 --- a/pydis_site/apps/api/models/bot/nomination.py +++ b/pydis_site/apps/api/models/bot/nomination.py @@ -34,7 +34,8 @@ class Nomination(ModelReprMixin, models.Model): ) end_reason = models.TextField( help_text="Why the nomination was ended.", - default="" + default="", + blank=True ) ended_at = models.DateTimeField( auto_now_add=False, -- cgit v1.2.3 From 9344d8ac63df05d67a79fbeacc7a8c31acf86853 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sun, 20 Sep 2020 06:35:38 +1000 Subject: Change documentation link model to order by package. --- pydis_site/apps/api/models/bot/documentation_link.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/bot/documentation_link.py b/pydis_site/apps/api/models/bot/documentation_link.py index 5a46460b..2a0ce751 100644 --- a/pydis_site/apps/api/models/bot/documentation_link.py +++ b/pydis_site/apps/api/models/bot/documentation_link.py @@ -24,3 +24,8 @@ class DocumentationLink(ModelReprMixin, models.Model): def __str__(self): """Returns the package and URL for the current documentation link, for display purposes.""" return f"{self.package} - {self.base_url}" + + class Meta: + """Defines the meta options for the documentation link model.""" + + ordering = ['package'] -- cgit v1.2.3 From c262b80c24450b7f2b24eb51b994495107e1aac6 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 3 Oct 2020 10:09:46 +0300 Subject: Logs Cleanup: Remove log entry model --- pydis_site/apps/api/models/__init__.py | 1 - pydis_site/apps/api/models/log_entry.py | 55 --------------------------------- 2 files changed, 56 deletions(-) delete mode 100644 pydis_site/apps/api/models/log_entry.py (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py index e3f928e1..0a8c90f6 100644 --- a/pydis_site/apps/api/models/__init__.py +++ b/pydis_site/apps/api/models/__init__.py @@ -14,4 +14,3 @@ from .bot import ( Role, User ) -from .log_entry import LogEntry diff --git a/pydis_site/apps/api/models/log_entry.py b/pydis_site/apps/api/models/log_entry.py deleted file mode 100644 index 752cd2ca..00000000 --- a/pydis_site/apps/api/models/log_entry.py +++ /dev/null @@ -1,55 +0,0 @@ -from django.db import models -from django.utils import timezone - -from pydis_site.apps.api.models.mixins import ModelReprMixin - - -class LogEntry(ModelReprMixin, models.Model): - """A log entry generated by one of the PyDis applications.""" - - application = models.CharField( - max_length=20, - help_text="The application that generated this log entry.", - choices=( - ('bot', 'Bot'), - ('seasonalbot', 'Seasonalbot'), - ('site', 'Website') - ) - ) - logger_name = models.CharField( - max_length=100, - help_text="The name of the logger that generated this log entry." - ) - timestamp = models.DateTimeField( - default=timezone.now, - help_text="The date and time when this entry was created." - ) - level = models.CharField( - max_length=8, # 'critical' - choices=( - ('debug', 'Debug'), - ('info', 'Info'), - ('warning', 'Warning'), - ('error', 'Error'), - ('critical', 'Critical') - ), - help_text=( - "The logger level at which this entry was emitted. The levels " - "correspond to the Python `logging` levels." - ) - ) - module = models.CharField( - max_length=100, - help_text="The fully qualified path of the module generating this log line." - ) - line = models.PositiveSmallIntegerField( - help_text="The line at which the log line was emitted." - ) - message = models.TextField( - help_text="The textual content of the log line." - ) - - class Meta: - """Customizes the default generated plural name to valid English.""" - - verbose_name_plural = 'Log entries' -- cgit v1.2.3 From 484eba7715fcbcc195d66f5a60ff56c8167ecf0e Mon Sep 17 00:00:00 2001 From: Lucas Lindström Date: Thu, 8 Oct 2020 00:17:47 +0200 Subject: Broke out metricity connection into an abstraction and added metricity endpoint unit tests. --- .coveragerc | 1 + pydis_site/apps/api/models/bot/metricity.py | 42 +++++++++++++++++++++ pydis_site/apps/api/tests/test_users.py | 58 +++++++++++++++++++++++++++++ pydis_site/apps/api/viewsets/bot/user.py | 17 +++++---- 4 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 pydis_site/apps/api/models/bot/metricity.py (limited to 'pydis_site/apps/api/models') diff --git a/.coveragerc b/.coveragerc index f5ddf08d..0cccc47c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -12,6 +12,7 @@ omit = */admin.py */apps.py */urls.py + pydis_site/apps/api/models/bot/metricity.py pydis_site/wsgi.py pydis_site/settings.py pydis_site/utils/resources.py diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py new file mode 100644 index 00000000..25b42fa2 --- /dev/null +++ b/pydis_site/apps/api/models/bot/metricity.py @@ -0,0 +1,42 @@ +from django.db import connections + + +class NotFound(Exception): + """Raised when an entity cannot be found.""" + + pass + + +class Metricity: + """Abstraction for a connection to the metricity database.""" + + def __init__(self): + self.cursor = connections['metricity'].cursor() + + def __enter__(self): + return self + + def __exit__(self, *_): + self.cursor.close() + + def user(self, user_id: str) -> dict: + """Query a user's data.""" + columns = ["verified_at"] + query = f"SELECT {','.join(columns)} FROM users WHERE id = '%s'" + self.cursor.execute(query, [user_id]) + values = self.cursor.fetchone() + + if not values: + raise NotFound() + + return dict(zip(columns, values)) + + def total_messages(self, user_id: str) -> int: + """Query total number of messages for a user.""" + self.cursor.execute("SELECT COUNT(*) FROM messages WHERE author_id = '%s'", [user_id]) + values = self.cursor.fetchone() + + if not values: + raise NotFound() + + return values[0] diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py index a02fce8a..76a21d3a 100644 --- a/pydis_site/apps/api/tests/test_users.py +++ b/pydis_site/apps/api/tests/test_users.py @@ -1,7 +1,10 @@ +from unittest.mock import patch + from django_hosts.resolvers import reverse from .base import APISubdomainTestCase from ..models import Role, User +from ..models.bot.metricity import NotFound class UnauthedUserAPITests(APISubdomainTestCase): @@ -170,3 +173,58 @@ class UserModelTests(APISubdomainTestCase): def test_correct_username_formatting(self): """Tests the username property with both name and discriminator formatted together.""" self.assertEqual(self.user_with_roles.username, "Test User with two roles#0001") + + +class UserMetricityTests(APISubdomainTestCase): + @classmethod + def setUpTestData(cls): + User.objects.create( + id=0, + name="Test user", + discriminator=1, + in_guild=True, + ) + + def test_get_metricity_data(self): + # Given + verified_at = "foo" + total_messages = 1 + self.mock_metricity_user(verified_at, total_messages) + + # When + url = reverse('bot:user-metricity-data', args=[0], host='api') + response = self.client.get(url) + + # Then + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), { + "verified_at": verified_at, + "total_messages": total_messages, + }) + + def test_no_metricity_user(self): + # Given + self.mock_no_metricity_user() + + # When + url = reverse('bot:user-metricity-data', args=[0], host='api') + response = self.client.get(url) + + # Then + self.assertEqual(response.status_code, 404) + + def mock_metricity_user(self, verified_at, total_messages): + patcher = patch("pydis_site.apps.api.viewsets.bot.user.Metricity") + self.metricity = patcher.start() + self.addCleanup(patcher.stop) + self.metricity = self.metricity.return_value.__enter__.return_value + self.metricity.user.return_value = dict(verified_at=verified_at) + self.metricity.total_messages.return_value = total_messages + + def mock_no_metricity_user(self): + patcher = patch("pydis_site.apps.api.viewsets.bot.user.Metricity") + self.metricity = patcher.start() + self.addCleanup(patcher.stop) + self.metricity = self.metricity.return_value.__enter__.return_value + self.metricity.user.side_effect = NotFound() + self.metricity.total_messages.side_effect = NotFound() diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 1b1af841..352d77c0 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -1,4 +1,3 @@ -from django.db import connections from rest_framework import status from rest_framework.decorators import action from rest_framework.request import Request @@ -6,6 +5,7 @@ from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet from rest_framework_bulk import BulkCreateModelMixin +from pydis_site.apps.api.models.bot.metricity import Metricity, NotFound from pydis_site.apps.api.models.bot.user import User from pydis_site.apps.api.serializers import UserSerializer @@ -142,10 +142,11 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): def metricity_data(self, request: Request, pk: str = None) -> Response: """Request handler for metricity_data endpoint.""" user = self.get_object() - with connections['metricity'].cursor() as cursor: - data = {} - cursor.execute("SELECT verified_at FROM users WHERE id = '%s'", [user.id]) - data["verified_at"], = cursor.fetchone() - cursor.execute("SELECT COUNT(*) FROM messages WHERE author_id = '%s'", [user.id]) - data["total_messages"], = cursor.fetchone() - return Response(data, status=status.HTTP_200_OK) + with Metricity() as metricity: + try: + data = metricity.user(user.id) + data["total_messages"] = metricity.total_messages(user.id) + return Response(data, status=status.HTTP_200_OK) + except NotFound: + return Response(dict(detail="User not found in metricity"), + status=status.HTTP_404_NOT_FOUND) -- cgit v1.2.3 From 9c812d5c40813b453bf0711538eaebea46e2b16b Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 10 Oct 2020 19:09:31 +0300 Subject: Add voice ban to infraction types and create migration for it --- .../migrations/0067_add_voice_ban_infraction_type.py | 18 ++++++++++++++++++ pydis_site/apps/api/models/bot/infraction.py | 3 ++- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 pydis_site/apps/api/migrations/0067_add_voice_ban_infraction_type.py (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/migrations/0067_add_voice_ban_infraction_type.py b/pydis_site/apps/api/migrations/0067_add_voice_ban_infraction_type.py new file mode 100644 index 00000000..9a940ff4 --- /dev/null +++ b/pydis_site/apps/api/migrations/0067_add_voice_ban_infraction_type.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.10 on 2020-10-10 16:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0066_merge_20201003_0730'), + ] + + operations = [ + migrations.AlterField( + model_name='infraction', + name='type', + field=models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('watch', 'Watch'), ('mute', 'Mute'), ('kick', 'Kick'), ('ban', 'Ban'), ('superstar', 'Superstar'), ('voice_ban', 'Voice Ban')], help_text='The type of the infraction.', max_length=9), + ), + ] diff --git a/pydis_site/apps/api/models/bot/infraction.py b/pydis_site/apps/api/models/bot/infraction.py index 7660cbba..5ef3a4ce 100644 --- a/pydis_site/apps/api/models/bot/infraction.py +++ b/pydis_site/apps/api/models/bot/infraction.py @@ -15,7 +15,8 @@ class Infraction(ModelReprMixin, models.Model): ("mute", "Mute"), ("kick", "Kick"), ("ban", "Ban"), - ("superstar", "Superstar") + ("superstar", "Superstar"), + ("voice_ban", "Voice Ban") ) inserted_at = models.DateTimeField( default=timezone.now, -- cgit v1.2.3 From 74f2131d05fdaa27b841e32f6c2fce2926b60f70 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 10 Oct 2020 20:34:13 +0300 Subject: Add trailing comma to infraction types listing Co-authored-by: Joe Banks --- pydis_site/apps/api/models/bot/infraction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/bot/infraction.py b/pydis_site/apps/api/models/bot/infraction.py index 5ef3a4ce..60c1e8dd 100644 --- a/pydis_site/apps/api/models/bot/infraction.py +++ b/pydis_site/apps/api/models/bot/infraction.py @@ -16,7 +16,7 @@ class Infraction(ModelReprMixin, models.Model): ("kick", "Kick"), ("ban", "Ban"), ("superstar", "Superstar"), - ("voice_ban", "Voice Ban") + ("voice_ban", "Voice Ban"), ) inserted_at = models.DateTimeField( default=timezone.now, -- cgit v1.2.3 From 730efe6cff7b61610d97bc6d3401acf5617bd17b Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 18 Oct 2020 19:38:32 +0100 Subject: Exclude deleted messages from total message count --- pydis_site/apps/api/models/bot/metricity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py index 25b42fa2..cdfbb499 100644 --- a/pydis_site/apps/api/models/bot/metricity.py +++ b/pydis_site/apps/api/models/bot/metricity.py @@ -33,7 +33,7 @@ class Metricity: def total_messages(self, user_id: str) -> int: """Query total number of messages for a user.""" - self.cursor.execute("SELECT COUNT(*) FROM messages WHERE author_id = '%s'", [user_id]) + self.cursor.execute("SELECT COUNT(*) FROM messages WHERE author_id = '%s' AND NOT is_deleted", [user_id]) values = self.cursor.fetchone() if not values: -- cgit v1.2.3 From d0853d28b34789756d03f414dc33fa84bc774142 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 18 Oct 2020 19:42:39 +0100 Subject: Split query into multiple lines for total messages --- pydis_site/apps/api/models/bot/metricity.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py index cdfbb499..eed1deb4 100644 --- a/pydis_site/apps/api/models/bot/metricity.py +++ b/pydis_site/apps/api/models/bot/metricity.py @@ -33,7 +33,10 @@ class Metricity: def total_messages(self, user_id: str) -> int: """Query total number of messages for a user.""" - self.cursor.execute("SELECT COUNT(*) FROM messages WHERE author_id = '%s' AND NOT is_deleted", [user_id]) + self.cursor.execute( + "SELECT COUNT(*) FROM messages WHERE author_id = '%s' AND NOT is_deleted", + [user_id] + ) values = self.cursor.fetchone() if not values: -- cgit v1.2.3 From 9db412a09da490093a1dfdcc6036b8cd7fa619ee Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Tue, 27 Oct 2020 01:51:27 +0000 Subject: Add message block query --- pydis_site/apps/api/models/bot/metricity.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py index eed1deb4..afbcbad8 100644 --- a/pydis_site/apps/api/models/bot/metricity.py +++ b/pydis_site/apps/api/models/bot/metricity.py @@ -1,5 +1,7 @@ from django.db import connections +BLOCK_INTERVAL = 10 * 60 # 10 minute blocks + class NotFound(Exception): """Raised when an entity cannot be found.""" @@ -43,3 +45,25 @@ class Metricity: raise NotFound() return values[0] + + def total_message_blocks(self, user_id: str) -> int: + """Query number of 10 minute blocks the user has been active during.""" + self.cursor.execute( + """ + SELECT + COUNT(*) + FROM ( + SELECT + to_timestamp(floor((extract('epoch' from created_at) / 600 )) * 600) + AT TIME ZONE 'UTC' AS interval + FROM messages + WHERE author_id='%s' AND NOT is_deleted GROUP BY interval) block_query; + """, + [user_id] + ) + values = self.cursor.fetchone() + + if not values: + raise NotFound() + + return values[0] -- cgit v1.2.3 From 2337453ef485017b519c47f31129063e0a0de011 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Tue, 27 Oct 2020 01:57:06 +0000 Subject: Clean up SQL query --- pydis_site/apps/api/models/bot/metricity.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py index afbcbad8..93d0df71 100644 --- a/pydis_site/apps/api/models/bot/metricity.py +++ b/pydis_site/apps/api/models/bot/metricity.py @@ -57,7 +57,11 @@ class Metricity: to_timestamp(floor((extract('epoch' from created_at) / 600 )) * 600) AT TIME ZONE 'UTC' AS interval FROM messages - WHERE author_id='%s' AND NOT is_deleted GROUP BY interval) block_query; + WHERE + author_id='%s' + AND NOT is_deleted + GROUP BY interval + ) block_query; """, [user_id] ) -- cgit v1.2.3 From 8c7eab5966089a64dcf0899c4f5c28462f745cdb Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Tue, 27 Oct 2020 02:11:30 +0000 Subject: Constant was never used :man_facepalming: --- pydis_site/apps/api/models/bot/metricity.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py index 93d0df71..81d6e788 100644 --- a/pydis_site/apps/api/models/bot/metricity.py +++ b/pydis_site/apps/api/models/bot/metricity.py @@ -49,12 +49,15 @@ class Metricity: def total_message_blocks(self, user_id: str) -> int: """Query number of 10 minute blocks the user has been active during.""" self.cursor.execute( - """ + f""" SELECT COUNT(*) FROM ( SELECT - to_timestamp(floor((extract('epoch' from created_at) / 600 )) * 600) + to_timestamp( + floor((extract('epoch' from created_at) / {BLOCK_INTERVAL} )) + * {BLOCK_INTERVAL} + ) AT TIME ZONE 'UTC' AS interval FROM messages WHERE -- cgit v1.2.3 From 2ef2a936ca43dd615cf6fb831b868d38abb58bae Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Tue, 27 Oct 2020 02:16:40 +0000 Subject: Use SQL formatting instead of f-strings for injecting values --- pydis_site/apps/api/models/bot/metricity.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py index 81d6e788..6f03baf0 100644 --- a/pydis_site/apps/api/models/bot/metricity.py +++ b/pydis_site/apps/api/models/bot/metricity.py @@ -55,8 +55,8 @@ class Metricity: FROM ( SELECT to_timestamp( - floor((extract('epoch' from created_at) / {BLOCK_INTERVAL} )) - * {BLOCK_INTERVAL} + floor((extract('epoch' from created_at) / %d )) + * %d ) AT TIME ZONE 'UTC' AS interval FROM messages @@ -66,7 +66,7 @@ class Metricity: GROUP BY interval ) block_query; """, - [user_id] + [BLOCK_INTERVAL, BLOCK_INTERVAL, user_id] ) values = self.cursor.fetchone() -- cgit v1.2.3 From a6f3a94cab63b52fd450442bbc0b24fdafbbb580 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Tue, 27 Oct 2020 02:19:54 +0000 Subject: Remove unnecessary f-string marker --- pydis_site/apps/api/models/bot/metricity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py index 6f03baf0..4313d2e2 100644 --- a/pydis_site/apps/api/models/bot/metricity.py +++ b/pydis_site/apps/api/models/bot/metricity.py @@ -49,7 +49,7 @@ class Metricity: def total_message_blocks(self, user_id: str) -> int: """Query number of 10 minute blocks the user has been active during.""" self.cursor.execute( - f""" + """ SELECT COUNT(*) FROM ( -- cgit v1.2.3 From 09d775c57b69c661a8de692c1b4a417331d5ac57 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Tue, 27 Oct 2020 02:43:59 +0000 Subject: Remove unnecessary timestamp conversion --- pydis_site/apps/api/models/bot/metricity.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py index 4313d2e2..60b2e73d 100644 --- a/pydis_site/apps/api/models/bot/metricity.py +++ b/pydis_site/apps/api/models/bot/metricity.py @@ -54,11 +54,7 @@ class Metricity: COUNT(*) FROM ( SELECT - to_timestamp( - floor((extract('epoch' from created_at) / %d )) - * %d - ) - AT TIME ZONE 'UTC' AS interval + (floor((extract('epoch' from created_at) / %d )) * %d) AS interval FROM messages WHERE author_id='%s' -- cgit v1.2.3 From 3595f62f00ef8786317c4af1f3522bd13f7656a1 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Fri, 30 Oct 2020 23:57:32 +0000 Subject: Update docstring in metricity block query --- pydis_site/apps/api/models/bot/metricity.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py index 60b2e73d..bb1db708 100644 --- a/pydis_site/apps/api/models/bot/metricity.py +++ b/pydis_site/apps/api/models/bot/metricity.py @@ -47,7 +47,11 @@ class Metricity: return values[0] def total_message_blocks(self, user_id: str) -> int: - """Query number of 10 minute blocks the user has been active during.""" + """ + Query number of 10 minute blocks during which the user has been active. + + This metric prevents users from spamming to achieve the message total threshold. + """ self.cursor.execute( """ SELECT -- cgit v1.2.3 From 7968dca8d3b6a9ae43e888bc045af211425acb28 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Fri, 30 Oct 2020 23:57:51 +0000 Subject: Indent metricity block query correctly --- pydis_site/apps/api/models/bot/metricity.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py index bb1db708..e7fc92fc 100644 --- a/pydis_site/apps/api/models/bot/metricity.py +++ b/pydis_site/apps/api/models/bot/metricity.py @@ -55,15 +55,15 @@ class Metricity: self.cursor.execute( """ SELECT - COUNT(*) - FROM ( - SELECT - (floor((extract('epoch' from created_at) / %d )) * %d) AS interval - FROM messages - WHERE - author_id='%s' - AND NOT is_deleted - GROUP BY interval + COUNT(*) + FROM ( + SELECT + (floor((extract('epoch' from created_at) / %d )) * %d) AS interval + FROM messages + WHERE + author_id='%s' + AND NOT is_deleted + GROUP BY interval ) block_query; """, [BLOCK_INTERVAL, BLOCK_INTERVAL, user_id] -- cgit v1.2.3 From d8e387b40cd6f3987a0815bf46b7b19888e83be9 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sat, 31 Oct 2020 00:13:08 +0000 Subject: Change format character in metricity data endpoint --- pydis_site/apps/api/models/bot/metricity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py index e7fc92fc..379b0757 100644 --- a/pydis_site/apps/api/models/bot/metricity.py +++ b/pydis_site/apps/api/models/bot/metricity.py @@ -58,7 +58,7 @@ class Metricity: COUNT(*) FROM ( SELECT - (floor((extract('epoch' from created_at) / %d )) * %d) AS interval + (floor((extract('epoch' from created_at) / %s )) * %s) AS interval FROM messages WHERE author_id='%s' -- cgit v1.2.3 From 23bf022cf8fae66225e829c4a0abbccb2940edb7 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Mon, 2 Nov 2020 18:25:27 +0000 Subject: Exclude bot commands and seasonalbot commands from voice gate --- postgres/init.sql | 7 +++++-- pydis_site/apps/api/models/bot/metricity.py | 20 +++++++++++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) (limited to 'pydis_site/apps/api/models') diff --git a/postgres/init.sql b/postgres/init.sql index 40538492..c77fec9e 100644 --- a/postgres/init.sql +++ b/postgres/init.sql @@ -18,6 +18,7 @@ CREATE TABLE messages ( author_id varchar references users(id), is_deleted boolean, created_at timestamp, + channel_id varchar, primary key(id) ); @@ -25,12 +26,14 @@ INSERT INTO messages VALUES( 0, 0, false, - now() + now(), + '267659945086812160' ); INSERT INTO messages VALUES( 1, 0, false, - now() + INTERVAL '10 minutes' + now() + INTERVAL '10 minutes,', + '1234' ); diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py index 379b0757..29d03d8b 100644 --- a/pydis_site/apps/api/models/bot/metricity.py +++ b/pydis_site/apps/api/models/bot/metricity.py @@ -2,6 +2,11 @@ from django.db import connections BLOCK_INTERVAL = 10 * 60 # 10 minute blocks +EXCLUDE_CHANNELS = [ + "267659945086812160", # Bot commands + "607247579608121354" # SeasonalBot commands +] + class NotFound(Exception): """Raised when an entity cannot be found.""" @@ -36,8 +41,16 @@ class Metricity: def total_messages(self, user_id: str) -> int: """Query total number of messages for a user.""" self.cursor.execute( - "SELECT COUNT(*) FROM messages WHERE author_id = '%s' AND NOT is_deleted", - [user_id] + """ + SELECT + COUNT(*) + FROM messages + WHERE + author_id = '%s' + AND NOT is_deleted + AND NOT %s::varchar[] @> ARRAY[channel_id] + """, + [user_id, EXCLUDE_CHANNELS] ) values = self.cursor.fetchone() @@ -63,10 +76,11 @@ class Metricity: WHERE author_id='%s' AND NOT is_deleted + AND NOT %s::varchar[] @> ARRAY[channel_id] GROUP BY interval ) block_query; """, - [BLOCK_INTERVAL, BLOCK_INTERVAL, user_id] + [BLOCK_INTERVAL, BLOCK_INTERVAL, user_id, EXCLUDE_CHANNELS] ) values = self.cursor.fetchone() -- cgit v1.2.3 From e1008870619f92bc76ed61eb9adbc6ada795b23f Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sat, 19 Dec 2020 03:40:25 +0000 Subject: Swap verified_at for joined_at --- pydis_site/apps/api/models/bot/metricity.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py index 29d03d8b..cae630f1 100644 --- a/pydis_site/apps/api/models/bot/metricity.py +++ b/pydis_site/apps/api/models/bot/metricity.py @@ -28,7 +28,8 @@ class Metricity: def user(self, user_id: str) -> dict: """Query a user's data.""" - columns = ["verified_at"] + # TODO: Swap this back to some sort of verified at date + columns = ["joined_at"] query = f"SELECT {','.join(columns)} FROM users WHERE id = '%s'" self.cursor.execute(query, [user_id]) values = self.cursor.fetchone() -- cgit v1.2.3 From 9ac477385d5ed26d2d8e4f711b2c927cfaf35461 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Mon, 22 Feb 2021 08:21:18 +0200 Subject: Split nomination model to 2 tables and create migrations --- .../api/migrations/0068_split_nomination_tables.py | 60 ++++++++++++++++++++++ .../0069_change_nomination_entry_plural.py | 17 ++++++ pydis_site/apps/api/models/bot/nomination.py | 47 ++++++++++++----- 3 files changed, 112 insertions(+), 12 deletions(-) create mode 100644 pydis_site/apps/api/migrations/0068_split_nomination_tables.py create mode 100644 pydis_site/apps/api/migrations/0069_change_nomination_entry_plural.py (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/migrations/0068_split_nomination_tables.py b/pydis_site/apps/api/migrations/0068_split_nomination_tables.py new file mode 100644 index 00000000..2e2313ee --- /dev/null +++ b/pydis_site/apps/api/migrations/0068_split_nomination_tables.py @@ -0,0 +1,60 @@ +# Generated by Django 3.0.11 on 2021-02-21 15:32 + +from django.apps.registry import Apps +from django.db import backends, migrations, models +from django.db.backends.base.schema import BaseDatabaseSchemaEditor +import django.db.models.deletion +import pydis_site.apps.api.models.mixins + + +def migrate_nominations(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: + Nomination = apps.get_model("api", "Nomination") + NominationEntry = apps.get_model("api", "NominationEntry") + + for nomination in Nomination.objects.all(): + nomination_entry = NominationEntry( + nomination=nomination, + actor=nomination.actor, + reason=nomination.reason, + inserted_at=nomination.inserted_at + ) + nomination_entry.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0067_add_voice_ban_infraction_type'), + ] + + operations = [ + migrations.CreateModel( + name='NominationEntry', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('reason', models.TextField(blank=True, help_text='Why the actor nominated this user.', null=True)), + ('inserted_at', + models.DateTimeField(auto_now_add=True, help_text='The creation date of this nomination entry.')), + ('actor', models.ForeignKey(help_text='The staff member that nominated this user.', + on_delete=django.db.models.deletion.CASCADE, related_name='nomination_set', + to='api.User')), + ('nomination', models.ForeignKey(help_text='Nomination to what this entry belongs.', + on_delete=django.db.models.deletion.CASCADE, to='api.Nomination')), + ], + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), + ), + migrations.RunPython(migrate_nominations), + migrations.RemoveField( + model_name='nomination', + name='actor', + ), + migrations.RemoveField( + model_name='nomination', + name='reason', + ), + migrations.AddField( + model_name='nomination', + name='reviewed', + field=models.BooleanField(default=False, help_text='Whether voting message have been made.'), + ), + ] diff --git a/pydis_site/apps/api/migrations/0069_change_nomination_entry_plural.py b/pydis_site/apps/api/migrations/0069_change_nomination_entry_plural.py new file mode 100644 index 00000000..6bf4ac8c --- /dev/null +++ b/pydis_site/apps/api/migrations/0069_change_nomination_entry_plural.py @@ -0,0 +1,17 @@ +# Generated by Django 3.0.11 on 2021-02-21 16:44 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0068_split_nomination_tables'), + ] + + operations = [ + migrations.AlterModelOptions( + name='nominationentry', + options={'verbose_name_plural': 'nomination entries'}, + ), + ] diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py index 11b9e36e..ed6f7d81 100644 --- a/pydis_site/apps/api/models/bot/nomination.py +++ b/pydis_site/apps/api/models/bot/nomination.py @@ -5,23 +5,12 @@ from pydis_site.apps.api.models.mixins import ModelReprMixin class Nomination(ModelReprMixin, models.Model): - """A helper nomination created by staff.""" + """A general helper nomination information created by staff.""" active = models.BooleanField( default=True, help_text="Whether this nomination is still relevant." ) - actor = models.ForeignKey( - User, - on_delete=models.CASCADE, - help_text="The staff member that nominated this user.", - related_name='nomination_set' - ) - reason = models.TextField( - help_text="Why this user was nominated.", - null=True, - blank=True - ) user = models.ForeignKey( User, on_delete=models.CASCADE, @@ -42,6 +31,10 @@ class Nomination(ModelReprMixin, models.Model): help_text="When the nomination was ended.", null=True ) + reviewed = models.BooleanField( + default=False, + help_text="Whether voting message have been made." + ) def __str__(self): """Representation that makes the target and state of the nomination immediately evident.""" @@ -52,3 +45,33 @@ class Nomination(ModelReprMixin, models.Model): """Set the ordering of nominations to most recent first.""" ordering = ("-inserted_at",) + + +class NominationEntry(ModelReprMixin, models.Model): + """A nomination entry created by single staff.""" + + nomination = models.ForeignKey( + Nomination, + on_delete=models.CASCADE, + help_text="Nomination to what this entry belongs." + ) + actor = models.ForeignKey( + User, + on_delete=models.CASCADE, + help_text="The staff member that nominated this user.", + related_name='nomination_set' + ) + reason = models.TextField( + help_text="Why the actor nominated this user.", + null=True, + blank=True + ) + inserted_at = models.DateTimeField( + auto_now_add=True, + help_text="The creation date of this nomination entry." + ) + + class Meta: + """Meta options for NominationEntry model.""" + + verbose_name_plural = "nomination entries" -- cgit v1.2.3 From 6591270f832e93a3eaf941aa1aeeddb6af7bce36 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Mon, 22 Feb 2021 11:36:25 +0200 Subject: Import NominationEntry to models __init__.py --- pydis_site/apps/api/models/__init__.py | 1 + pydis_site/apps/api/models/bot/__init__.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py index 0a8c90f6..fd5bf220 100644 --- a/pydis_site/apps/api/models/__init__.py +++ b/pydis_site/apps/api/models/__init__.py @@ -8,6 +8,7 @@ from .bot import ( Message, MessageDeletionContext, Nomination, + NominationEntry, OffensiveMessage, OffTopicChannelName, Reminder, diff --git a/pydis_site/apps/api/models/bot/__init__.py b/pydis_site/apps/api/models/bot/__init__.py index 1673b434..ac864de3 100644 --- a/pydis_site/apps/api/models/bot/__init__.py +++ b/pydis_site/apps/api/models/bot/__init__.py @@ -6,7 +6,7 @@ from .documentation_link import DocumentationLink from .infraction import Infraction from .message import Message from .message_deletion_context import MessageDeletionContext -from .nomination import Nomination +from .nomination import Nomination, NominationEntry from .off_topic_channel_name import OffTopicChannelName from .offensive_message import OffensiveMessage from .reminder import Reminder -- cgit v1.2.3 From f731e4dd31f9eba8e8c9916329a5c08578055077 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 24 Feb 2021 08:27:36 +0200 Subject: Set related_name option of nomination field of NominationEntry In order to use entries in serializer without manually setting entries key we have to use related_name option to automatically fetch all related entries. --- pydis_site/apps/api/models/bot/nomination.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py index ed6f7d81..a813855d 100644 --- a/pydis_site/apps/api/models/bot/nomination.py +++ b/pydis_site/apps/api/models/bot/nomination.py @@ -53,7 +53,8 @@ class NominationEntry(ModelReprMixin, models.Model): nomination = models.ForeignKey( Nomination, on_delete=models.CASCADE, - help_text="Nomination to what this entry belongs." + help_text="Nomination to what this entry belongs.", + related_name="entries" ) actor = models.ForeignKey( User, -- cgit v1.2.3 From 1fc5470a0af18aab427a48faf132bea2712330aa Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 24 Feb 2021 08:28:54 +0200 Subject: Set default ordering of NominationEntry to inserted_at decreasing Set it here so we don't have to set it every place where we fetch entries. --- pydis_site/apps/api/models/bot/nomination.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py index a813855d..e72a18ca 100644 --- a/pydis_site/apps/api/models/bot/nomination.py +++ b/pydis_site/apps/api/models/bot/nomination.py @@ -76,3 +76,7 @@ class NominationEntry(ModelReprMixin, models.Model): """Meta options for NominationEntry model.""" verbose_name_plural = "nomination entries" + + # Set default ordering here to latest first + # so we don't need to define it everywhere + ordering = ("-inserted_at",) -- cgit v1.2.3 From cd03f7448ae4c96c23a80c4c46344bc35b8c603a Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 24 Feb 2021 08:44:22 +0200 Subject: Make default value of nomination entry reason to empty string For string fields NULL as default is not suggested, so use empty string instead. --- pydis_site/apps/api/migrations/0068_split_nomination_tables.py | 2 +- pydis_site/apps/api/models/bot/nomination.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/migrations/0068_split_nomination_tables.py b/pydis_site/apps/api/migrations/0068_split_nomination_tables.py index 107e3a56..27c29017 100644 --- a/pydis_site/apps/api/migrations/0068_split_nomination_tables.py +++ b/pydis_site/apps/api/migrations/0068_split_nomination_tables.py @@ -45,7 +45,7 @@ class Migration(migrations.Migration): name='NominationEntry', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('reason', models.TextField(blank=True, help_text='Why the actor nominated this user.', null=True)), + ('reason', models.TextField(blank=True, help_text='Why the actor nominated this user.', default="")), ('inserted_at', models.DateTimeField(auto_now_add=True, help_text='The creation date of this nomination entry.')), ('actor', models.ForeignKey(help_text='The staff member that nominated this user.', diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py index e72a18ca..443200ff 100644 --- a/pydis_site/apps/api/models/bot/nomination.py +++ b/pydis_site/apps/api/models/bot/nomination.py @@ -64,7 +64,7 @@ class NominationEntry(ModelReprMixin, models.Model): ) reason = models.TextField( help_text="Why the actor nominated this user.", - null=True, + default="", blank=True ) inserted_at = models.DateTimeField( -- cgit v1.2.3 From 53a9f5282adfdd3a2a4ebc819fcc9cd568a632ea Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 6 Mar 2021 14:09:38 +0200 Subject: Fix grammar of nomination models --- pydis_site/apps/api/migrations/0068_split_nomination_tables.py | 4 ++-- pydis_site/apps/api/models/bot/nomination.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) (limited to 'pydis_site/apps/api/models') diff --git a/pydis_site/apps/api/migrations/0068_split_nomination_tables.py b/pydis_site/apps/api/migrations/0068_split_nomination_tables.py index 27c29017..79825ed7 100644 --- a/pydis_site/apps/api/migrations/0068_split_nomination_tables.py +++ b/pydis_site/apps/api/migrations/0068_split_nomination_tables.py @@ -51,7 +51,7 @@ class Migration(migrations.Migration): ('actor', models.ForeignKey(help_text='The staff member that nominated this user.', on_delete=django.db.models.deletion.CASCADE, related_name='nomination_set', to='api.User')), - ('nomination', models.ForeignKey(help_text='Nomination to what this entry belongs.', + ('nomination', models.ForeignKey(help_text='The nomination this entry belongs to.', on_delete=django.db.models.deletion.CASCADE, to='api.Nomination', related_name='entries')), ], @@ -70,6 +70,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='nomination', name='reviewed', - field=models.BooleanField(default=False, help_text='Whether voting message have been made.'), + field=models.BooleanField(default=False, help_text='Whether a review was made.'), ), ] diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py index 443200ff..221d8534 100644 --- a/pydis_site/apps/api/models/bot/nomination.py +++ b/pydis_site/apps/api/models/bot/nomination.py @@ -33,7 +33,7 @@ class Nomination(ModelReprMixin, models.Model): ) reviewed = models.BooleanField( default=False, - help_text="Whether voting message have been made." + help_text="Whether a review was made." ) def __str__(self): @@ -48,12 +48,12 @@ class Nomination(ModelReprMixin, models.Model): class NominationEntry(ModelReprMixin, models.Model): - """A nomination entry created by single staff.""" + """A nomination entry created by a single staff member.""" nomination = models.ForeignKey( Nomination, on_delete=models.CASCADE, - help_text="Nomination to what this entry belongs.", + help_text="The nomination this entry belongs to.", related_name="entries" ) actor = models.ForeignKey( -- cgit v1.2.3