aboutsummaryrefslogtreecommitdiffstats
path: root/pydis_site
diff options
context:
space:
mode:
authorGravatar Leon Sandøy <[email protected]>2019-04-16 14:16:38 +0200
committerGravatar Leon Sandøy <[email protected]>2019-04-16 14:16:38 +0200
commitda7a5701f1dae277aab4c4947fe7d23fe02987bd (patch)
treef8f8c683664a02a9fd1860c8bb99b6645e74a651 /pydis_site
parentrefactoring slightly to use a class-based view, changing home to main. Ready ... (diff)
parentMerge pull request #207 from python-discord/api/split-models (diff)
merging in changes from the main branch
Diffstat (limited to 'pydis_site')
-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/migrations/0035_create_table_log_entry.py29
-rw-r--r--pydis_site/apps/api/models.py452
-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.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/serializers.py21
-rw-r--r--pydis_site/apps/api/tests/test_validators.py6
-rw-r--r--pydis_site/apps/api/urls.py12
-rw-r--r--pydis_site/apps/api/viewsets.py57
-rw-r--r--pydis_site/apps/main/tests.py9
30 files changed, 716 insertions, 491 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/migrations/0035_create_table_log_entry.py b/pydis_site/apps/api/migrations/0035_create_table_log_entry.py
new file mode 100644
index 00000000..a8256a0e
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0035_create_table_log_entry.py
@@ -0,0 +1,29 @@
+# Generated by Django 2.1.5 on 2019-04-08 18:27
+
+from django.db import migrations, models
+import django.utils.timezone
+import pydis_site.apps.api.models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0034_add_botsetting_name_validator'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='LogEntry',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('application', models.CharField(choices=[('bot', 'Bot'), ('seasonalbot', 'Seasonalbot'), ('site', 'Website')], help_text='The application that generated this log entry.', max_length=20)),
+ ('logger_name', models.CharField(help_text='The name of the logger that generated this log entry.', max_length=100)),
+ ('timestamp', models.DateTimeField(default=django.utils.timezone.now, help_text='The date and time when this entry was created.')),
+ ('level', models.CharField(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.', max_length=8)),
+ ('module', models.CharField(help_text='The fully qualified path of the module generating this log line.', max_length=100)),
+ ('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.')),
+ ],
+ bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model),
+ ),
+ ]
diff --git a/pydis_site/apps/api/models.py b/pydis_site/apps/api/models.py
deleted file mode 100644
index 86c99f86..00000000
--- a/pydis_site/apps/api/models.py
+++ /dev/null
@@ -1,452 +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."
- )
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.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/serializers.py b/pydis_site/apps/api/serializers.py
index 5b3cb28c..8f045044 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -4,12 +4,12 @@ from rest_framework_bulk import BulkSerializerMixin
from .models import (
BotSetting, DeletedMessage,
DocumentationLink, Infraction,
- MessageDeletionContext, Nomination,
- OffTopicChannelName, Reminder,
- Role, SnakeFact,
- SnakeIdiom, SnakeName,
- SpecialSnake, Tag,
- User
+ LogEntry, MessageDeletionContext,
+ Nomination, OffTopicChannelName,
+ Reminder, Role,
+ SnakeFact, SnakeIdiom,
+ SnakeName, SpecialSnake,
+ Tag, User
)
@@ -101,6 +101,15 @@ class ExpandedInfractionSerializer(InfractionSerializer):
return ret
+class LogEntrySerializer(ModelSerializer):
+ class Meta:
+ model = LogEntry
+ fields = (
+ 'application', 'logger_name', 'timestamp',
+ 'level', 'module', 'line', 'message'
+ )
+
+
class OffTopicChannelNameSerializer(ModelSerializer):
class Meta:
model = OffTopicChannelName
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 = (
diff --git a/pydis_site/apps/api/urls.py b/pydis_site/apps/api/urls.py
index 6c89a52e..724d7e2b 100644
--- a/pydis_site/apps/api/urls.py
+++ b/pydis_site/apps/api/urls.py
@@ -5,11 +5,12 @@ from .views import HealthcheckView, RulesView
from .viewsets import (
BotSettingViewSet, DeletedMessageViewSet,
DocumentationLinkViewSet, InfractionViewSet,
- NominationViewSet, OffTopicChannelNameViewSet,
- ReminderViewSet, RoleViewSet,
- SnakeFactViewSet, SnakeIdiomViewSet,
- SnakeNameViewSet, SpecialSnakeViewSet,
- TagViewSet, UserViewSet
+ LogEntryViewSet, NominationViewSet,
+ OffTopicChannelNameViewSet, ReminderViewSet,
+ RoleViewSet, SnakeFactViewSet,
+ SnakeIdiomViewSet, SnakeNameViewSet,
+ SpecialSnakeViewSet, TagViewSet,
+ UserViewSet
)
@@ -81,6 +82,7 @@ urlpatterns = (
# from django_hosts.resolvers import reverse
# snake_name_endpoint = reverse('bot:snakename-list', host='api') # `bot/` endpoints
path('bot/', include((bot_router.urls, 'api'), namespace='bot')),
+ path('logs', LogEntryViewSet.as_view({'post': 'create'}), name='logs'),
path('healthcheck', HealthcheckView.as_view(), name='healthcheck'),
path('rules', RulesView.as_view(), name='rules')
)
diff --git a/pydis_site/apps/api/viewsets.py b/pydis_site/apps/api/viewsets.py
index 949ffaaa..47915256 100644
--- a/pydis_site/apps/api/viewsets.py
+++ b/pydis_site/apps/api/viewsets.py
@@ -15,22 +15,23 @@ from rest_framework_bulk import BulkCreateModelMixin
from .models import (
BotSetting, DocumentationLink,
- Infraction, MessageDeletionContext,
- Nomination, OffTopicChannelName,
- Reminder, Role,
- SnakeFact, SnakeIdiom,
- SnakeName, SpecialSnake,
- Tag, User
+ Infraction, LogEntry,
+ MessageDeletionContext, Nomination,
+ OffTopicChannelName, Reminder,
+ Role, SnakeFact,
+ SnakeIdiom, SnakeName,
+ SpecialSnake, Tag,
+ User
)
from .serializers import (
BotSettingSerializer, DocumentationLinkSerializer,
ExpandedInfractionSerializer, InfractionSerializer,
- MessageDeletionContextSerializer, NominationSerializer,
- OffTopicChannelNameSerializer, ReminderSerializer,
- RoleSerializer, SnakeFactSerializer,
- SnakeIdiomSerializer, SnakeNameSerializer,
- SpecialSnakeSerializer, TagSerializer,
- UserSerializer
+ LogEntrySerializer, MessageDeletionContextSerializer,
+ NominationSerializer, OffTopicChannelNameSerializer,
+ ReminderSerializer, RoleSerializer,
+ SnakeFactSerializer, SnakeIdiomSerializer,
+ SnakeNameSerializer, SpecialSnakeSerializer,
+ TagSerializer, UserSerializer
)
@@ -280,6 +281,38 @@ class InfractionViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
return self.partial_update(*args, **kwargs)
+class LogEntryViewSet(CreateModelMixin, GenericViewSet):
+ """
+ View providing support for creating log entries in the site database
+ for viewing via the log browser.
+
+ ## Routes
+ ### POST /logs
+ Create a new log entry.
+
+ #### Request body
+ >>> {
+ ... 'application': str, # 'bot' | 'seasonalbot' | 'site'
+ ... 'logger_name': str, # such as 'bot.cogs.moderation'
+ ... 'timestamp': Optional[str], # from `datetime.utcnow().isoformat()`
+ ... 'level': str, # 'debug' | 'info' | 'warning' | 'error' | 'critical'
+ ... 'module': str, # such as 'pydis_site.apps.api.serializers'
+ ... 'line': int, # > 0
+ ... 'message': str, # textual formatted content of the logline
+ ... }
+
+ #### Status codes
+ - 201: returned on success
+ - 400: if the request body has invalid fields, see the response for details
+
+ ## Authentication
+ Requires a API token.
+ """
+
+ queryset = LogEntry.objects.all()
+ serializer_class = LogEntrySerializer
+
+
class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet):
"""
View of off-topic channel names used by the bot
diff --git a/pydis_site/apps/main/tests.py b/pydis_site/apps/main/tests.py
index 54fac6e8..733ddaa3 100644
--- a/pydis_site/apps/main/tests.py
+++ b/pydis_site/apps/main/tests.py
@@ -1,9 +1,16 @@
from django.test import TestCase
from django_hosts.resolvers import reverse
+from pydis_site.apps.home.templatetags import extra_filters
+
class TestIndexReturns200(TestCase):
def test_index_returns_200(self):
- url = reverse('index')
+ url = reverse('home.index')
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
+
+
+class TestExtraFilterTemplateTags(TestCase):
+ def test_starts_with(self):
+ self.assertTrue(extra_filters.starts_with('foo', 'f'))