aboutsummaryrefslogtreecommitdiffstats
path: root/pydis_site/apps
diff options
context:
space:
mode:
authorGravatar Johannes Christ <[email protected]>2019-04-14 21:52:41 +0200
committerGravatar Johannes Christ <[email protected]>2019-04-14 22:00:44 +0200
commit5002b44c76ed66d9d1ed898b302e489473b143d0 (patch)
treef615fd754afa8680ffb5d5ab85ed895557437548 /pydis_site/apps
parentAdd missing test. (diff)
Move models to submodules.
Diffstat (limited to 'pydis_site/apps')
-rw-r--r--pydis_site/apps/api/migrations/0008_tag_embed_validator.py4
-rw-r--r--pydis_site/apps/api/migrations/0019_deletedmessage.py3
-rw-r--r--pydis_site/apps/api/migrations/0034_add_botsetting_name_validator.py4
-rw-r--r--pydis_site/apps/api/models.py498
-rw-r--r--pydis_site/apps/api/models/__init__.py20
-rw-r--r--pydis_site/apps/api/models/bot/__init__.py16
-rw-r--r--pydis_site/apps/api/models/bot/bot_setting.py27
-rw-r--r--pydis_site/apps/api/models/bot/deleted_message.py12
-rw-r--r--pydis_site/apps/api/models/bot/documentation_link.py25
-rw-r--r--pydis_site/apps/api/models/bot/infraction 3.py67
-rw-r--r--pydis_site/apps/api/models/bot/infraction.py67
-rw-r--r--pydis_site/apps/api/models/bot/message.py51
-rw-r--r--pydis_site/apps/api/models/bot/message_deletion_context.py23
-rw-r--r--pydis_site/apps/api/models/bot/nomination.py33
-rw-r--r--pydis_site/apps/api/models/bot/off_topic_channel_name.py16
-rw-r--r--pydis_site/apps/api/models/bot/reminder.py44
-rw-r--r--pydis_site/apps/api/models/bot/role.py48
-rw-r--r--pydis_site/apps/api/models/bot/snake_fact.py16
-rw-r--r--pydis_site/apps/api/models/bot/snake_idiom.py16
-rw-r--r--pydis_site/apps/api/models/bot/snake_name.py23
-rw-r--r--pydis_site/apps/api/models/bot/special_snake.py26
-rw-r--r--pydis_site/apps/api/models/bot/tag.py (renamed from pydis_site/apps/api/validators.py)25
-rw-r--r--pydis_site/apps/api/models/bot/user.py52
-rw-r--r--pydis_site/apps/api/models/log_entry.py50
-rw-r--r--pydis_site/apps/api/models/utils.py20
-rw-r--r--pydis_site/apps/api/tests/test_validators.py6
26 files changed, 679 insertions, 513 deletions
diff --git a/pydis_site/apps/api/migrations/0008_tag_embed_validator.py b/pydis_site/apps/api/migrations/0008_tag_embed_validator.py
index ea8f03d2..d53ddb90 100644
--- a/pydis_site/apps/api/migrations/0008_tag_embed_validator.py
+++ b/pydis_site/apps/api/migrations/0008_tag_embed_validator.py
@@ -1,6 +1,6 @@
# Generated by Django 2.1.1 on 2018-09-23 10:07
-import pydis_site.apps.api.validators
+import pydis_site.apps.api.models.bot.tag
import django.contrib.postgres.fields.jsonb
from django.db import migrations
@@ -15,6 +15,6 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='tag',
name='embed',
- field=django.contrib.postgres.fields.jsonb.JSONField(help_text='The actual embed shown by this tag.', validators=[pydis_site.apps.api.validators.validate_tag_embed]),
+ field=django.contrib.postgres.fields.jsonb.JSONField(help_text='The actual embed shown by this tag.', validators=[pydis_site.apps.api.models.bot.tag.validate_tag_embed]),
),
]
diff --git a/pydis_site/apps/api/migrations/0019_deletedmessage.py b/pydis_site/apps/api/migrations/0019_deletedmessage.py
index f451ecf4..4b028f0c 100644
--- a/pydis_site/apps/api/migrations/0019_deletedmessage.py
+++ b/pydis_site/apps/api/migrations/0019_deletedmessage.py
@@ -1,7 +1,6 @@
# Generated by Django 2.1.1 on 2018-11-18 20:26
import pydis_site.apps.api.models
-import pydis_site.apps.api.validators
from django.db import migrations, models
import django.db.models.deletion
@@ -19,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.validators.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.bot.tag.validate_tag_embed]), help_text='Embeds attached to this message.', size=None)),
('author', models.ForeignKey(help_text='The author of this message.', on_delete=django.db.models.deletion.CASCADE, to='api.User')),
('deletion_context', models.ForeignKey(help_text='The deletion context this message is part of.', on_delete=django.db.models.deletion.CASCADE, to='api.MessageDeletionContext')),
],
diff --git a/pydis_site/apps/api/migrations/0034_add_botsetting_name_validator.py b/pydis_site/apps/api/migrations/0034_add_botsetting_name_validator.py
index bd370d8e..d2a98e5d 100644
--- a/pydis_site/apps/api/migrations/0034_add_botsetting_name_validator.py
+++ b/pydis_site/apps/api/migrations/0034_add_botsetting_name_validator.py
@@ -1,6 +1,6 @@
# Generated by Django 2.1.5 on 2019-02-18 19:41
-import pydis_site.apps.api.validators
+import pydis_site.apps.api.models.bot.bot_setting
from django.db import migrations, models
@@ -15,6 +15,6 @@ class Migration(migrations.Migration):
model_name='botsetting',
name='name',
field=models.CharField(max_length=50, primary_key=True, serialize=False, validators=[
- pydis_site.apps.api.validators.validate_bot_setting_name]),
+ pydis_site.apps.api.models.bot.bot_setting.validate_bot_setting_name]),
),
]
diff --git a/pydis_site/apps/api/models.py b/pydis_site/apps/api/models.py
deleted file mode 100644
index b2499f8d..00000000
--- a/pydis_site/apps/api/models.py
+++ /dev/null
@@ -1,498 +0,0 @@
-from operator import itemgetter
-
-from django.contrib.postgres import fields as pgfields
-from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
-from django.db import models
-from django.utils import timezone
-
-from .validators import validate_bot_setting_name, validate_tag_embed
-
-
-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})>'
-
-
-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."
- )
-
-
-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}"
-
-
-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
-
-
-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
-
-
-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
-
-
-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
-
-
-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})"
-
-
-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
-
-
-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
-
-
-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}"
-
-
-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
-
-
-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."
- )
-
-
-class DeletedMessage(Message):
- deletion_context = models.ForeignKey(
- MessageDeletionContext,
- help_text="The deletion context this message is part of.",
- on_delete=models.CASCADE
- )
-
-
-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
-
-
-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}"
-
-
-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."
- )
-
-
-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/__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/validators.py b/pydis_site/apps/api/models/bot/tag.py
index ea2112a9..62881ca2 100644
--- a/pydis_site/apps/api/validators.py
+++ b/pydis_site/apps/api/models/bot/tag.py
@@ -1,7 +1,11 @@
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):
@@ -155,10 +159,21 @@ def validate_tag_embed(embed):
validator(value)
-def validate_bot_setting_name(name):
- known_settings = (
- 'defcon',
+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,)
)
- if name not in known_settings:
- raise ValidationError(f"`{name}` is not a known setting name.")
+ 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})>'
diff --git a/pydis_site/apps/api/tests/test_validators.py b/pydis_site/apps/api/tests/test_validators.py
index d0b78c23..ffa2f61e 100644
--- a/pydis_site/apps/api/tests/test_validators.py
+++ b/pydis_site/apps/api/tests/test_validators.py
@@ -1,10 +1,8 @@
from django.core.exceptions import ValidationError
from django.test import TestCase
-from ..validators import (
- validate_bot_setting_name,
- validate_tag_embed
-)
+from ..models.bot.bot_setting import validate_bot_setting_name
+from ..models.bot.tag import validate_tag_embed
REQUIRED_KEYS = (