diff options
author | 2019-04-14 21:52:41 +0200 | |
---|---|---|
committer | 2019-04-14 22:00:44 +0200 | |
commit | 5002b44c76ed66d9d1ed898b302e489473b143d0 (patch) | |
tree | f615fd754afa8680ffb5d5ab85ed895557437548 /pydis_site/apps/api/models | |
parent | Add missing test. (diff) |
Move models to submodules.
Diffstat (limited to 'pydis_site/apps/api/models')
21 files changed, 831 insertions, 0 deletions
diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py new file mode 100644 index 00000000..4645bda2 --- /dev/null +++ b/pydis_site/apps/api/models/__init__.py @@ -0,0 +1,20 @@ +from .bot import ( # noqa + BotSetting, + DocumentationLink, + DeletedMessage, + Infraction, + Message, + MessageDeletionContext, + Nomination, + OffTopicChannelName, + Reminder, + Role, + SnakeFact, + SnakeIdiom, + SnakeName, + SpecialSnake, + Tag, + User +) +from .log_entry import LogEntry # noqa +from .utils import ModelReprMixin # noqa diff --git a/pydis_site/apps/api/models/bot/__init__.py b/pydis_site/apps/api/models/bot/__init__.py new file mode 100644 index 00000000..fb313193 --- /dev/null +++ b/pydis_site/apps/api/models/bot/__init__.py @@ -0,0 +1,16 @@ +from .bot_setting import BotSetting # noqa +from .deleted_message import DeletedMessage # noqa +from .documentation_link import DocumentationLink # noqa +from .infraction import Infraction # noqa +from .message import Message # noqa +from .message_deletion_context import MessageDeletionContext # noqa +from .nomination import Nomination # noqa +from .off_topic_channel_name import OffTopicChannelName # noqa +from .reminder import Reminder # noqa +from .role import Role # noqa +from .snake_fact import SnakeFact # noqa +from .snake_idiom import SnakeIdiom # noqa +from .snake_name import SnakeName # noqa +from .special_snake import SpecialSnake # noqa +from .tag import Tag # noqa +from .user import User # noqa diff --git a/pydis_site/apps/api/models/bot/bot_setting.py b/pydis_site/apps/api/models/bot/bot_setting.py new file mode 100644 index 00000000..a6eeaa1f --- /dev/null +++ b/pydis_site/apps/api/models/bot/bot_setting.py @@ -0,0 +1,27 @@ +from django.contrib.postgres import fields as pgfields +from django.core.exceptions import ValidationError +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin + + +def validate_bot_setting_name(name): + known_settings = ( + 'defcon', + ) + + if name not in known_settings: + raise ValidationError(f"`{name}` is not a known setting name.") + + +class BotSetting(ModelReprMixin, models.Model): + """A configuration entry for the bot.""" + + name = models.CharField( + primary_key=True, + max_length=50, + validators=(validate_bot_setting_name,) + ) + data = pgfields.JSONField( + help_text="The actual settings of this setting." + ) diff --git a/pydis_site/apps/api/models/bot/deleted_message.py b/pydis_site/apps/api/models/bot/deleted_message.py new file mode 100644 index 00000000..0f46cd12 --- /dev/null +++ b/pydis_site/apps/api/models/bot/deleted_message.py @@ -0,0 +1,12 @@ +from django.db import models + +from pydis_site.apps.api.models.bot.message import Message +from pydis_site.apps.api.models.bot.message_deletion_context import MessageDeletionContext + + +class DeletedMessage(Message): + deletion_context = models.ForeignKey( + MessageDeletionContext, + help_text="The deletion context this message is part of.", + on_delete=models.CASCADE + ) diff --git a/pydis_site/apps/api/models/bot/documentation_link.py b/pydis_site/apps/api/models/bot/documentation_link.py new file mode 100644 index 00000000..d7df22ad --- /dev/null +++ b/pydis_site/apps/api/models/bot/documentation_link.py @@ -0,0 +1,25 @@ +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class DocumentationLink(ModelReprMixin, models.Model): + """A documentation link used by the `!docs` command of the bot.""" + + package = models.CharField( + primary_key=True, + max_length=50, + help_text="The Python package name that this documentation link belongs to." + ) + base_url = models.URLField( + help_text=( + "The base URL from which documentation will be available for this project. " + "Used to generate links to various symbols within this package." + ) + ) + inventory_url = models.URLField( + help_text="The URL at which the Sphinx inventory is available for this package." + ) + + def __str__(self): + return f"{self.package} - {self.base_url}" diff --git a/pydis_site/apps/api/models/bot/infraction 3.py b/pydis_site/apps/api/models/bot/infraction 3.py new file mode 100644 index 00000000..76d7b881 --- /dev/null +++ b/pydis_site/apps/api/models/bot/infraction 3.py @@ -0,0 +1,67 @@ +from django.db import models +from django.utils import timezone + +from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.bot.user import User + + +class Infraction(ModelReprMixin, models.Model): + """An infraction for a Discord user.""" + + TYPE_CHOICES = ( + ("note", "Note"), + ("warning", "Warning"), + ("watch", "Watch"), + ("mute", "Mute"), + ("kick", "Kick"), + ("ban", "Ban"), + ("superstar", "Superstar") + ) + inserted_at = models.DateTimeField( + default=timezone.now, + help_text="The date and time of the creation of this infraction." + ) + expires_at = models.DateTimeField( + null=True, + help_text=( + "The date and time of the expiration of this infraction. " + "Null if the infraction is permanent or it can't expire." + ) + ) + active = models.BooleanField( + default=True, + help_text="Whether the infraction is still active." + ) + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name='infractions_received', + help_text="The user to which the infraction was applied." + ) + actor = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name='infractions_given', + help_text="The user which applied the infraction." + ) + type = models.CharField( + max_length=9, + choices=TYPE_CHOICES, + help_text="The type of the infraction." + ) + reason = models.TextField( + null=True, + help_text="The reason for the infraction." + ) + hidden = models.BooleanField( + default=False, + help_text="Whether the infraction is a shadow infraction." + ) + + def __str__(self): + s = f"#{self.id}: {self.type} on {self.user_id}" + if self.expires_at: + s += f" until {self.expires_at}" + if self.hidden: + s += " (hidden)" + return s diff --git a/pydis_site/apps/api/models/bot/infraction.py b/pydis_site/apps/api/models/bot/infraction.py new file mode 100644 index 00000000..911ca589 --- /dev/null +++ b/pydis_site/apps/api/models/bot/infraction.py @@ -0,0 +1,67 @@ +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 + + +class Infraction(ModelReprMixin, models.Model): + """An infraction for a Discord user.""" + + TYPE_CHOICES = ( + ("note", "Note"), + ("warning", "Warning"), + ("watch", "Watch"), + ("mute", "Mute"), + ("kick", "Kick"), + ("ban", "Ban"), + ("superstar", "Superstar") + ) + inserted_at = models.DateTimeField( + default=timezone.now, + help_text="The date and time of the creation of this infraction." + ) + expires_at = models.DateTimeField( + null=True, + help_text=( + "The date and time of the expiration of this infraction. " + "Null if the infraction is permanent or it can't expire." + ) + ) + active = models.BooleanField( + default=True, + help_text="Whether the infraction is still active." + ) + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name='infractions_received', + help_text="The user to which the infraction was applied." + ) + actor = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name='infractions_given', + help_text="The user which applied the infraction." + ) + type = models.CharField( + max_length=9, + choices=TYPE_CHOICES, + help_text="The type of the infraction." + ) + reason = models.TextField( + null=True, + help_text="The reason for the infraction." + ) + hidden = models.BooleanField( + default=False, + help_text="Whether the infraction is a shadow infraction." + ) + + def __str__(self): + s = f"#{self.id}: {self.type} on {self.user_id}" + if self.expires_at: + s += f" until {self.expires_at}" + if self.hidden: + s += " (hidden)" + return s diff --git a/pydis_site/apps/api/models/bot/message.py b/pydis_site/apps/api/models/bot/message.py new file mode 100644 index 00000000..22500be0 --- /dev/null +++ b/pydis_site/apps/api/models/bot/message.py @@ -0,0 +1,51 @@ +from django.contrib.postgres import fields as pgfields +from django.core.validators import MinValueValidator +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.bot.tag import validate_tag_embed +from pydis_site.apps.api.models.bot.user import User + + +class Message(ModelReprMixin, models.Model): + id = models.BigIntegerField( + primary_key=True, + help_text="The message ID as taken from Discord.", + validators=( + MinValueValidator( + limit_value=0, + message="Message IDs cannot be negative." + ), + ) + ) + author = models.ForeignKey( + User, + on_delete=models.CASCADE, + help_text="The author of this message." + ) + channel_id = models.BigIntegerField( + help_text=( + "The channel ID that this message was " + "sent in, taken from Discord." + ), + validators=( + MinValueValidator( + limit_value=0, + message="Channel IDs cannot be negative." + ), + ) + ) + content = models.CharField( + max_length=2_000, + help_text="The content of this message, taken from Discord.", + blank=True + ) + embeds = pgfields.ArrayField( + pgfields.JSONField( + validators=(validate_tag_embed,) + ), + help_text="Embeds attached to this message." + ) + + class Meta: + abstract = True diff --git a/pydis_site/apps/api/models/bot/message_deletion_context.py b/pydis_site/apps/api/models/bot/message_deletion_context.py new file mode 100644 index 00000000..9904ef71 --- /dev/null +++ b/pydis_site/apps/api/models/bot/message_deletion_context.py @@ -0,0 +1,23 @@ +from django.db import models + +from pydis_site.apps.api.models.bot.user import User +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class MessageDeletionContext(ModelReprMixin, models.Model): + actor = models.ForeignKey( + User, + on_delete=models.CASCADE, + help_text=( + "The original actor causing this deletion. Could be the author " + "of a manual clean command invocation, the bot when executing " + "automatic actions, or nothing to indicate that the bulk " + "deletion was not issued by us." + ), + null=True + ) + creation = models.DateTimeField( + # Consider whether we want to add a validator here that ensures + # the deletion context does not take place in the future. + help_text="When this deletion took place." + ) diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py new file mode 100644 index 00000000..5ebb9759 --- /dev/null +++ b/pydis_site/apps/api/models/bot/nomination.py @@ -0,0 +1,33 @@ +from django.db import models + +from pydis_site.apps.api.models.bot.user import User +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class Nomination(ModelReprMixin, models.Model): + """A helper nomination created by staff.""" + + active = models.BooleanField( + default=True, + help_text="Whether this nomination is still relevant." + ) + author = 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." + ) + user = models.OneToOneField( + User, + on_delete=models.CASCADE, + help_text="The nominated user.", + primary_key=True, + related_name='nomination' + ) + inserted_at = models.DateTimeField( + auto_now_add=True, + help_text="The creation date of this nomination." + ) 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 new file mode 100644 index 00000000..dff7eaf8 --- /dev/null +++ b/pydis_site/apps/api/models/bot/off_topic_channel_name.py @@ -0,0 +1,16 @@ +from django.core.validators import RegexValidator +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class OffTopicChannelName(ModelReprMixin, models.Model): + name = models.CharField( + primary_key=True, + max_length=96, + validators=(RegexValidator(regex=r'^[a-z0-9-]+$'),), + help_text="The actual channel name that will be used on our Discord server." + ) + + def __str__(self): + return self.name diff --git a/pydis_site/apps/api/models/bot/reminder.py b/pydis_site/apps/api/models/bot/reminder.py new file mode 100644 index 00000000..abccdf82 --- /dev/null +++ b/pydis_site/apps/api/models/bot/reminder.py @@ -0,0 +1,44 @@ +from django.core.validators import MinValueValidator +from django.db import models + +from pydis_site.apps.api.models.bot.user import User +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class Reminder(ModelReprMixin, models.Model): + """A reminder created by a user.""" + + active = models.BooleanField( + default=True, + help_text=( + "Whether this reminder is still active. " + "If not, it has been sent out to the user." + ) + ) + author = models.ForeignKey( + User, + on_delete=models.CASCADE, + help_text="The creator of this reminder." + ) + channel_id = models.BigIntegerField( + help_text=( + "The channel ID that this message was " + "sent in, taken from Discord." + ), + validators=( + MinValueValidator( + limit_value=0, + message="Channel IDs cannot be negative." + ), + ) + ) + content = models.CharField( + max_length=1500, + help_text="The content that the user wants to be reminded of." + ) + expiration = models.DateTimeField( + help_text="When this reminder should be sent." + ) + + def __str__(self): + return f"{self.content} on {self.expiration} by {self.author}" diff --git a/pydis_site/apps/api/models/bot/role.py b/pydis_site/apps/api/models/bot/role.py new file mode 100644 index 00000000..8106394f --- /dev/null +++ b/pydis_site/apps/api/models/bot/role.py @@ -0,0 +1,48 @@ +from django.core.validators import MaxValueValidator, MinValueValidator +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class Role(ModelReprMixin, models.Model): + """A role on our Discord server.""" + + id = models.BigIntegerField( # noqa + primary_key=True, + validators=( + MinValueValidator( + limit_value=0, + message="Role IDs cannot be negative." + ), + ), + help_text="The role ID, taken from Discord." + ) + name = models.CharField( + max_length=100, + help_text="The role name, taken from Discord." + ) + colour = models.IntegerField( + validators=( + MinValueValidator( + limit_value=0, + message="Colour hex cannot be negative." + ), + ), + help_text="The integer value of the colour of this role from Discord." + ) + permissions = models.IntegerField( + validators=( + MinValueValidator( + limit_value=0, + message="Role permissions cannot be negative." + ), + MaxValueValidator( + limit_value=2 << 32, + message="Role permission bitset exceeds value of having all permissions" + ) + ), + help_text="The integer value of the permission bitset of this role from Discord." + ) + + def __str__(self): + return self.name diff --git a/pydis_site/apps/api/models/bot/snake_fact.py b/pydis_site/apps/api/models/bot/snake_fact.py new file mode 100644 index 00000000..4398620a --- /dev/null +++ b/pydis_site/apps/api/models/bot/snake_fact.py @@ -0,0 +1,16 @@ +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class SnakeFact(ModelReprMixin, models.Model): + """A snake fact used by the bot's snake cog.""" + + fact = models.CharField( + primary_key=True, + max_length=200, + help_text="A fact about snakes." + ) + + def __str__(self): + return self.fact diff --git a/pydis_site/apps/api/models/bot/snake_idiom.py b/pydis_site/apps/api/models/bot/snake_idiom.py new file mode 100644 index 00000000..e4db00e0 --- /dev/null +++ b/pydis_site/apps/api/models/bot/snake_idiom.py @@ -0,0 +1,16 @@ +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class SnakeIdiom(ModelReprMixin, models.Model): + """A snake idiom used by the snake cog.""" + + idiom = models.CharField( + primary_key=True, + max_length=140, + help_text="A saying about a snake." + ) + + def __str__(self): + return self.idiom diff --git a/pydis_site/apps/api/models/bot/snake_name.py b/pydis_site/apps/api/models/bot/snake_name.py new file mode 100644 index 00000000..045a7faa --- /dev/null +++ b/pydis_site/apps/api/models/bot/snake_name.py @@ -0,0 +1,23 @@ +from django.core.validators import RegexValidator +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class SnakeName(ModelReprMixin, models.Model): + """A snake name used by the bot's snake cog.""" + + name = models.CharField( + primary_key=True, + max_length=100, + help_text="The regular name for this snake, e.g. 'Python'.", + validators=[RegexValidator(regex=r'^([^0-9])+$')] + ) + scientific = models.CharField( + max_length=150, + help_text="The scientific name for this snake, e.g. 'Python bivittatus'.", + validators=[RegexValidator(regex=r'^([^0-9])+$')] + ) + + def __str__(self): + return f"{self.name} ({self.scientific})" diff --git a/pydis_site/apps/api/models/bot/special_snake.py b/pydis_site/apps/api/models/bot/special_snake.py new file mode 100644 index 00000000..1406b9e0 --- /dev/null +++ b/pydis_site/apps/api/models/bot/special_snake.py @@ -0,0 +1,26 @@ +from django.contrib.postgres import fields as pgfields +from django.core.validators import RegexValidator +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class SpecialSnake(ModelReprMixin, models.Model): + """A special snake's name, info and image from our database used by the bot's snake cog.""" + + name = models.CharField( + max_length=140, + primary_key=True, + help_text='A special snake name.', + validators=[RegexValidator(regex=r'^([^0-9])+$')] + ) + info = models.TextField( + help_text='Info about a special snake.' + ) + images = pgfields.ArrayField( + models.URLField(), + help_text='Images displaying this special snake.' + ) + + def __str__(self): + return self.name diff --git a/pydis_site/apps/api/models/bot/tag.py b/pydis_site/apps/api/models/bot/tag.py new file mode 100644 index 00000000..62881ca2 --- /dev/null +++ b/pydis_site/apps/api/models/bot/tag.py @@ -0,0 +1,179 @@ +from collections.abc import Mapping + +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.utils import ModelReprMixin + + +def validate_tag_embed_fields(fields): + field_validators = { + 'name': (MaxLengthValidator(limit_value=256),), + 'value': (MaxLengthValidator(limit_value=1024),) + } + + for field in fields: + if not isinstance(field, Mapping): + raise ValidationError("Embed fields must be a mapping.") + + 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): + 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): + 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): + """ + 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.validators import validate_tag_embed + >>> class MyMessage(models.Model): + ... embed = pgfields.JSONField( + ... validators=( + ... validate_tag_embed, + ... ) + ... ) + ... # ... + ... + + Args: + embed (Dict[str, Union[str, List[dict], dict]]): + 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) + + +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): + return self.title diff --git a/pydis_site/apps/api/models/bot/user.py b/pydis_site/apps/api/models/bot/user.py new file mode 100644 index 00000000..f5365ed1 --- /dev/null +++ b/pydis_site/apps/api/models/bot/user.py @@ -0,0 +1,52 @@ +from django.core.validators import MaxValueValidator, MinValueValidator +from django.db import models + +from pydis_site.apps.api.models.bot.role import Role +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class User(ModelReprMixin, models.Model): + """A Discord user.""" + + id = models.BigIntegerField( # noqa + primary_key=True, + validators=( + MinValueValidator( + limit_value=0, + message="User IDs cannot be negative." + ), + ), + help_text="The ID of this user, taken from Discord." + ) + name = models.CharField( + max_length=32, + help_text="The username, taken from Discord." + ) + discriminator = models.PositiveSmallIntegerField( + validators=( + MaxValueValidator( + limit_value=9999, + message="Discriminators may not exceed `9999`." + ), + ), + help_text="The discriminator of this user, taken from Discord." + ) + avatar_hash = models.CharField( + max_length=100, + help_text=( + "The user's avatar hash, taken from Discord. " + "Null if the user does not have any custom avatar." + ), + null=True + ) + roles = models.ManyToManyField( + Role, + help_text="Any roles this user has on our server." + ) + in_guild = models.BooleanField( + default=True, + help_text="Whether this user is in our server." + ) + + def __str__(self): + return f"{self.name}#{self.discriminator}" diff --git a/pydis_site/apps/api/models/log_entry.py b/pydis_site/apps/api/models/log_entry.py new file mode 100644 index 00000000..acd7953a --- /dev/null +++ b/pydis_site/apps/api/models/log_entry.py @@ -0,0 +1,50 @@ +from django.db import models +from django.utils import timezone + +from pydis_site.apps.api.models.utils 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." + ) diff --git a/pydis_site/apps/api/models/utils.py b/pydis_site/apps/api/models/utils.py new file mode 100644 index 00000000..731486e7 --- /dev/null +++ b/pydis_site/apps/api/models/utils.py @@ -0,0 +1,20 @@ +from operator import itemgetter + + +class ModelReprMixin: + """ + Adds a `__repr__` method to the model subclassing this + mixin which will display the model's class name along + with all parameters used to construct the object. + """ + + def __repr__(self): + 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})>' |