From 5d98792d09e78b7f1a3ecdaba458a0c3c74a0faf Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 7 Apr 2019 22:57:28 +0200 Subject: Add the `LogEntry` model. --- .../api/migrations/0035_create_table_log_entry.py | 28 +++++++++++++++ pydis_site/apps/api/models.py | 42 ++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0035_create_table_log_entry.py 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..30ff1ffd --- /dev/null +++ b/pydis_site/apps/api/migrations/0035_create_table_log_entry.py @@ -0,0 +1,28 @@ +# Generated by Django 2.1.5 on 2019-04-07 20:53 + +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.')), + ], + 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 index 86c99f86..a541e4ab 100644 --- a/pydis_site/apps/api/models.py +++ b/pydis_site/apps/api/models.py @@ -450,3 +450,45 @@ class Nomination(ModelReprMixin, models.Model): 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." + ) -- cgit v1.2.3 From 5006641ce1527524e85f22c9b4dc0096fe2718b9 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 7 Apr 2019 23:32:19 +0200 Subject: Add the `/logs` API route. --- pydis_site/apps/api/serializers.py | 21 +++++++++++++++------ pydis_site/apps/api/urls.py | 12 +++++++----- pydis_site/apps/api/viewsets.py | 31 +++++++++++++++++++------------ 3 files changed, 41 insertions(+), 23 deletions(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 9a92313a..2b0a687f 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -5,12 +5,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 ) @@ -102,6 +102,15 @@ class ExpandedInfractionSerializer(InfractionSerializer): return ret +class LogEntrySerializer(ModelSerializer): + class Meta: + model = LogEntry + fields = ( + 'application', 'logger_name', 'timestamp', + 'level', 'module', 'line' + ) + + class OffTopicChannelNameSerializer(ModelSerializer): class Meta: model = OffTopicChannelName 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..b97f19b6 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,12 @@ class InfractionViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge return self.partial_update(*args, **kwargs) +class LogEntryViewSet(CreateModelMixin, GenericViewSet): + # TODO: doc me foobar baz boom bang crow caw caw caw + queryset = LogEntry.objects.all() + serializer_class = LogEntrySerializer + + class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): """ View of off-topic channel names used by the bot -- cgit v1.2.3 From 91294e94e1205ba32b12d0cebc899d1337583740 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 7 Apr 2019 23:34:42 +0200 Subject: Minor formatting improvements. --- pydis_site/apps/api/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pydis_site/apps/api/models.py b/pydis_site/apps/api/models.py index a541e4ab..35b76c4c 100644 --- a/pydis_site/apps/api/models.py +++ b/pydis_site/apps/api/models.py @@ -451,6 +451,7 @@ class Nomination(ModelReprMixin, models.Model): help_text="The creation date of this nomination." ) + class LogEntry(ModelReprMixin, models.Model): """A log entry generated by one of the PyDis applications.""" -- cgit v1.2.3 From 2a4e5e606520fca4649b2a7c40e73e8380e4798d Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Mon, 8 Apr 2019 20:28:48 +0200 Subject: Add the `message` field. --- pydis_site/apps/api/migrations/0035_create_table_log_entry.py | 3 ++- pydis_site/apps/api/models.py | 3 +++ pydis_site/apps/api/serializers.py | 3 +-- 3 files changed, 6 insertions(+), 3 deletions(-) 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 index 30ff1ffd..a8256a0e 100644 --- a/pydis_site/apps/api/migrations/0035_create_table_log_entry.py +++ b/pydis_site/apps/api/migrations/0035_create_table_log_entry.py @@ -1,4 +1,4 @@ -# Generated by Django 2.1.5 on 2019-04-07 20:53 +# Generated by Django 2.1.5 on 2019-04-08 18:27 from django.db import migrations, models import django.utils.timezone @@ -22,6 +22,7 @@ class Migration(migrations.Migration): ('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 index 35b76c4c..b2499f8d 100644 --- a/pydis_site/apps/api/models.py +++ b/pydis_site/apps/api/models.py @@ -493,3 +493,6 @@ class LogEntry(ModelReprMixin, models.Model): 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/serializers.py b/pydis_site/apps/api/serializers.py index 2b0a687f..8f045044 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -1,5 +1,4 @@ from rest_framework.serializers import ModelSerializer, PrimaryKeyRelatedField, ValidationError -from rest_framework.validators import UniqueValidator from rest_framework_bulk import BulkSerializerMixin from .models import ( @@ -107,7 +106,7 @@ class LogEntrySerializer(ModelSerializer): model = LogEntry fields = ( 'application', 'logger_name', 'timestamp', - 'level', 'module', 'line' + 'level', 'module', 'line', 'message' ) -- cgit v1.2.3 From eff5b434c78fa0360a075f8f47c69d1992411d04 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Wed, 10 Apr 2019 19:40:35 +0200 Subject: Remove unused changelog. --- CHANGELOG.md | 39 --------------------------------------- 1 file changed, 39 deletions(-) delete mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index c59fe469..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,39 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## Unreleased - -### Added - -- Markdown linting - -### Changed - -- Added unneded files to `.dockerignore` - -## [0.3.0] - 2018-18-09 - -### Added - -- Do not recommend pushes to `master` in `CONTRIBUTING.md` - -- Documentation about how to set up the site and - Postgres up locally using Docker and `pip` - -- Healthchecks for the `app` container - -- Require 100% code coverage in `CONTRIBUTING.md` - -- Require `CHANGELOG.md` updates in `CONTRIBUTING.md` - -- The `psmgr` console script as a shortcut to `python manage.py` - -- This file - -### Changed - -- Improved build speed by not installing unneeded dependencies. -- cgit v1.2.3 From b476340a7c9d50aa7df607a19b4010e93dd51516 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Wed, 10 Apr 2019 19:42:52 +0200 Subject: Make the pipeline green as grass. --- pydis_site/apps/api/serializers.py | 1 - pydis_site/apps/api/tests/test_models.py | 16 ++++++++-------- pydis_site/apps/api/tests/test_validators.py | 1 + pydis_site/apps/api/validators.py | 4 ++-- pydis_site/settings.py | 6 +++--- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 9a92313a..5b3cb28c 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -1,5 +1,4 @@ from rest_framework.serializers import ModelSerializer, PrimaryKeyRelatedField, ValidationError -from rest_framework.validators import UniqueValidator from rest_framework_bulk import BulkSerializerMixin from .models import ( diff --git a/pydis_site/apps/api/tests/test_models.py b/pydis_site/apps/api/tests/test_models.py index 43d1eb41..a958419d 100644 --- a/pydis_site/apps/api/tests/test_models.py +++ b/pydis_site/apps/api/tests/test_models.py @@ -3,14 +3,14 @@ from datetime import datetime as dt, timezone from django.test import SimpleTestCase from ..models import ( - BotSetting, DeletedMessage, - DocumentationLink, Infraction, - Message, MessageDeletionContext, - ModelReprMixin, OffTopicChannelName, - Reminder, Role, - SnakeFact, SnakeIdiom, - SnakeName, SpecialSnake, - Tag, User + DeletedMessage, DocumentationLink, + Infraction, Message, + MessageDeletionContext, ModelReprMixin, + OffTopicChannelName, Reminder, + Role, SnakeFact, + SnakeIdiom, SnakeName, + SpecialSnake, Tag, + User ) diff --git a/pydis_site/apps/api/tests/test_validators.py b/pydis_site/apps/api/tests/test_validators.py index d2c0a136..d0b78c23 100644 --- a/pydis_site/apps/api/tests/test_validators.py +++ b/pydis_site/apps/api/tests/test_validators.py @@ -20,6 +20,7 @@ class BotSettingValidatorTests(TestCase): with self.assertRaises(ValidationError): validate_bot_setting_name('bad name') + class TagEmbedValidatorTests(TestCase): def test_rejects_non_mapping(self): with self.assertRaises(ValidationError): diff --git a/pydis_site/apps/api/validators.py b/pydis_site/apps/api/validators.py index 69a8d1ef..ea2112a9 100644 --- a/pydis_site/apps/api/validators.py +++ b/pydis_site/apps/api/validators.py @@ -156,9 +156,9 @@ def validate_tag_embed(embed): def validate_bot_setting_name(name): - KNOWN_SETTINGS = ( + known_settings = ( 'defcon', ) - if name not in KNOWN_SETTINGS: + if name not in known_settings: raise ValidationError(f"`{name}` is not a known setting name.") diff --git a/pydis_site/settings.py b/pydis_site/settings.py index e8355918..f7668f65 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -169,10 +169,10 @@ STATICFILES_DIRS = [os.path.join(BASE_DIR, 'pydis_site', 'static')] STATIC_ROOT = env('STATIC_ROOT', default='staticfiles') STATICFILES_FINDERS = [ - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', - 'django_simple_bulma.finders.SimpleBulmaFinder', + 'django_simple_bulma.finders.SimpleBulmaFinder', ] # django-hosts -- cgit v1.2.3 From e00b347ca513e83029110ef740657b2ecda028a2 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Wed, 10 Apr 2019 19:50:32 +0200 Subject: Document `LogEntryViewSet`. --- pydis_site/apps/api/viewsets.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/pydis_site/apps/api/viewsets.py b/pydis_site/apps/api/viewsets.py index b97f19b6..47915256 100644 --- a/pydis_site/apps/api/viewsets.py +++ b/pydis_site/apps/api/viewsets.py @@ -282,7 +282,33 @@ class InfractionViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge class LogEntryViewSet(CreateModelMixin, GenericViewSet): - # TODO: doc me foobar baz boom bang crow caw caw caw + """ + 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 -- cgit v1.2.3 From 21712f6a1bc6af2230db11954b041e9a307e0947 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Wed, 10 Apr 2019 21:50:34 +0200 Subject: Only install uWSGI in Docker. --- Pipfile | 1 - Pipfile.lock | 73 +++++++++++++++++++++++---------------------------- docker/app/Dockerfile | 3 ++- 3 files changed, 35 insertions(+), 42 deletions(-) diff --git a/Pipfile b/Pipfile index 932746d2..31cfdeba 100644 --- a/Pipfile +++ b/Pipfile @@ -23,7 +23,6 @@ django-filter = "~=2.1.0" django-hosts = "~=3.0" djangorestframework = "~=3.9.2" djangorestframework-bulk = "~=0.2.1" -uwsgi = "~=2.0.18" psycopg2-binary = "~=2.8" django-simple-bulma = ">=1.1.6,<2.0" django-crispy-bulma = ">=0.1.2,<2.0" diff --git a/Pipfile.lock b/Pipfile.lock index c574a6f1..00399c65 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "e7a956892e9d18b6ac8ab13fe8e139d29196e236fbb194f4cb8b37308ea91c6e" + "sha256": "c0d047b8e76560eb15bf978d943b8aae89a87f0feb8a2aa97fb3be97a21de17d" }, "pipfile-spec": 6, "requires": { @@ -110,44 +110,44 @@ }, "psycopg2-binary": { "hashes": [ - "sha256:0c8cb1b93e25eaf1dfedbcb4cee4ce3860035ce216b71590bda5f8dc99128526", - "sha256:1c2eeb074d2be404f22a14c4c71eeaa1a855c940abedf6f726158348e9c83dd6", - "sha256:1d879395a5d0dfe191dcfc622dce8b0a5e4fb76d089c903f18a4913e5fbc79c7", - "sha256:20d47c61bc9d6a431039f6ceb3b9a34a952a1562cf718054f64c524526fb8ed8", - "sha256:39fc9323f065361b99fca7758ac723d7e66bbc7e6ec9c90e398857af0ef61404", - "sha256:3c5b7579f3075f19b0b54495d28105049d44564d67b817eef2fa561b2bcf532b", - "sha256:3f811db92e30ea2412dfba8e64b18102017646969b5f436138d7b2b38a0e8966", - "sha256:41d60c8610a70b6666641b662379ef3b847ad2acd38303d4c8e34efd0f782403", - "sha256:45979c708536a3132398863579280657c6bc77e9b9be8b05ba0dae9013b5a0a8", - "sha256:4aaa54574b52b85223d3d950b2fc77bd672e6fbb324bb99f834eacbedc4545f7", - "sha256:50647aa5f7171153a5f7fa667f99f55468b9b663b997927e4d2e83955b21aa9f", - "sha256:528175ab1f12131bb5ea0df64fc524a4c6c51c197dc68d2a9e646029890d4d0f", - "sha256:5cbb49cc1c3c4c69ba09a7e18452bd44371b6adad0c9636f117a7554660af529", - "sha256:6e2f69635b548147e9b9298f5b67155d212f742683e51d78d24ceec4a3f5464d", - "sha256:7994d43431f1b9eba5daa1bdb8f626482cf01e379c00967092c6ebb3e4d3235f", - "sha256:86ec556a75f7e0124581100f2c4c8f9c8d67fc6254af4ce500633a77a4ca3207", - "sha256:9c32635fca3c250f5a3d2e424819419cd4a0f277c1a383b20fdd95e799d1da7c", - "sha256:9e19396065fdbbbc7c0b288a4e70694e1e63593388020fdb86076b12c315bda5", - "sha256:a9e7606233fa6c559491758cb319fab6cec25d931cdb5db670c434dde44ab56b", - "sha256:c914312ad7c923ac154821fbd591e8482ab03cdb190e14b05e30bf856f69e98c", - "sha256:d354ebb06f851f5f2cbc675bbb1369f71091aec6a894986d68341cbca59e7e56", - "sha256:d35a25989112c07a994070f1b3c711b19a14209c7608802eced3bcbf07c375bb", - "sha256:d71c128151c2d93fab36d7273b6a6696a63e0aa03ba3f7b1b0abb862c2344765", - "sha256:d77e4cbecc30f3a8406873c83075c5dae9dcd2ba1c0ffb088edd29372d3df84c", - "sha256:dd0b68d212d0992e2a906c6c34a1ef3f82b3dba74ff99744c77f390ffecb0cca", - "sha256:f0f97d3e0ab12456733687fc99d05e4de67f12d48a57c3baf1f5a1c6cd76c876", - "sha256:f7b72646a5a50aed8535d8cd2d7e915238f389c181d20143f67c2c6527ca5d0e", - "sha256:fd06663aa38b2b7b1f71017329545e17f2a583b127de4eeaabdc4cb16cf3a942" + "sha256:163d3ee445a0b4c0109877da9e46271aacf4e5e3d60ae7368669555c30f13e7c", + "sha256:1af0bfe7b0c13a0e613a27311fd4f9c5d024e8fc0f4b3d284e7df02a58a11fc0", + "sha256:2169c3a1bf52d5b30cc98625b5919a964c571a32e8646be20be6c7e3e82079de", + "sha256:218f079fa48e2ef812dc3d3ce6ec2f67ac56427ba4b038d5d6331f2cceb489c2", + "sha256:26a958930687e94c4c6c73c171e4d4783b82ae4e16aa3424e6bcd4529bceedf0", + "sha256:2c7c195aef3acdbc853942bc674844031a732890d2fee88a324298ed376b6c2b", + "sha256:2ecdbfed7004669472bfa27c8d51012c717c241c7154ae17e4c8f93024043525", + "sha256:345fc31b71a90ada1b51826537917b19a1af685a91c0f066787069c184d7d00f", + "sha256:378a06649503f548be5f1e9eec2e94cc1d6138250b82a08dcc6151bca8cec107", + "sha256:3f300bf2930e501dde09605de85cb2b84c2638e2c954be02a3c86f28176d3525", + "sha256:6c2f66c653ce8bbd7e789d0f7f92c3f9fea881b55226f0ae5ee550cce9e3cf0e", + "sha256:6fccbac2633831b877a8fbf865f7082d34895e82a015795a9f80f99a2efe2576", + "sha256:7a166f8ccb6888358d3e67795b057540ea7caa71ab9e089b0cb0097f01088965", + "sha256:8f6b84f887ec6fef6c1796779f8ec2603dc7e9ef52bc9269de719d4bcbdaebbb", + "sha256:92cf3ceb7bb90cf35b8bd993c640b15d4832ba0e142a3b9da5006ef217da595d", + "sha256:a20dfdf73f56da674926a3811929cff9fd23b9af90be9a6c36ac246a3486eef3", + "sha256:a84415df4689251556c961e4fe3b25d30e32f00faa8064ce0909458dbe0d67b2", + "sha256:ab1aa1cd50df3860f624c9713ee9e690eefd4e049d3a4d86577bab6e741e9616", + "sha256:abc9dcf85e75a8687f2a6d560c0c1a2593e8e34ba6f9ad6721f8212c5de179a2", + "sha256:c10454710a81a2f4b1ff4d1c83ac2cec63e0e55845a56324991514af5b1299d0", + "sha256:c38f80719e4dfae7a6311a4f091f07f4fb2fb5d602352015d5639f63f8fabb68", + "sha256:d75cf00605630b2cfefa5c62373c605dcda1cc0d607902847dbb8e8e9b67c1ce", + "sha256:dce15cb6ef604c9e38fdaa848f58f83153ade9f4aa5e4cf5812aa27163561750", + "sha256:e7e0db4311bb76bf3f6e0380f71912cfa6d0be7cc635e3772476050b0dabdabd", + "sha256:eac59cae78dfe3fbf7ece25c170d7a152f88df7643381aa5e7344c2028a8d8d4", + "sha256:ead7b3e1567bd14cacd44279c5e42cd19f54b9feed39180220253f4fbe3abd56", + "sha256:ed772a5e8e7e5dd6bede960a86940c17cf653c7f158dafa5d52e919b676f10ba", + "sha256:f2d73131acb94afa45de8b6b8a4bfb21bbe3736633d6478e53247f19dd8c299c" ], "index": "pypi", - "version": "==2.8" + "version": "==2.8.1" }, "pytz": { "hashes": [ - "sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9", - "sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c" + "sha256:303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", + "sha256:d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141" ], - "version": "==2018.9" + "version": "==2019.1" }, "six": { "hashes": [ @@ -162,13 +162,6 @@ "sha256:7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873" ], "version": "==0.3.0" - }, - "uwsgi": { - "hashes": [ - "sha256:4972ac538800fb2d421027f49b4a1869b66048839507ccf0aa2fda792d99f583" - ], - "index": "pypi", - "version": "==2.0.18" } }, "develop": { diff --git a/docker/app/Dockerfile b/docker/app/Dockerfile index 93d0c378..b12da08f 100644 --- a/docker/app/Dockerfile +++ b/docker/app/Dockerfile @@ -28,7 +28,8 @@ RUN rm -r /opt/bitnami/python/lib/python3.*/site-packages/setuptools* && \ pip install --no-cache-dir -U setuptools RUN python3 -m pip install pipenv \ - && python3 -m pipenv install --dev --system --deploy + && python3 -m pipenv install --dev --system --deploy \ + && pip install uwsgi==2.0.18 COPY . . -- cgit v1.2.3 From 44b453c35983ab74de2369dab32f8e23bd6177ce Mon Sep 17 00:00:00 2001 From: ImportErr Date: Fri, 4 Jan 2019 11:57:24 +0000 Subject: Added info for running the server --- docs/setup.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/setup.md b/docs/setup.md index d6e5a7bf..f92a24df 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -67,5 +67,7 @@ pip install -e .[lint,test] to install base dependencies along with lint and test dependencies. You can either use `python manage.py` directly, or you can use the console -entrypoint for it, `psmgr`. For example, to run tests, you could use either -`python manage.py test` or `psmgr test`. Happy hacking! +entrypoint for it, `psmgr`. For example, to run tests, you could use either `python manage.py test` or `psmgr test`. +To run the server, run `DEBUG=1 python manage.py runserver`. If it gives you an error saying +`django.core.exceptions.ImproperlyConfigured: Set the DATABASE_URL environment variable` please make sure the server your postgres database is on is running +and run the command `$(export cat .env) in your terminal`. Happy hacking! -- cgit v1.2.3 From 949f3d8932f5b87b93092d8654aaa0ac15a5200d Mon Sep 17 00:00:00 2001 From: ImportErr Date: Fri, 4 Jan 2019 14:11:32 +0000 Subject: Made clarifications on the hosts file --- docs/setup.md | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/docs/setup.md b/docs/setup.md index f92a24df..dfaf3122 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -26,6 +26,9 @@ in your environment variables: export DATABASE_URL=postgres://pysite@localhost/pysite ``` +After this step, inside the `.env` file, set the `SECRET_KEY` variable which can be anything you like. Also, set the `ENGINE` variable +to `django.db.backends.postgresql`. + A simpler approach to automatically configuring this might come in the near future - if you have any suggestions, please let us know! @@ -68,6 +71,23 @@ to install base dependencies along with lint and test dependencies. You can either use `python manage.py` directly, or you can use the console entrypoint for it, `psmgr`. For example, to run tests, you could use either `python manage.py test` or `psmgr test`. + +## Hosts file + +Make sure you add the following to your hosts file: + +```sh +127.0.0.1 pythondiscord.local +127.0.0.1 api.pythondiscord.local +127.0.0.1 staff.pythondiscord.local +127.0.0.1 admin.pythondiscord.local +127.0.0.1 wiki.pythondiscord.local +127.0.0.1 ws.pythondiscord.local +``` +When trying to access the site, you'll be using the domains above instead of the usual `localhost:8000`. + To run the server, run `DEBUG=1 python manage.py runserver`. If it gives you an error saying -`django.core.exceptions.ImproperlyConfigured: Set the DATABASE_URL environment variable` please make sure the server your postgres database is on is running -and run the command `$(export cat .env) in your terminal`. Happy hacking! +`django.core.exceptions.ImproperlyConfigured: Set the DATABASE_URL environment variable` please make sure the server that your postgres database is located at is running +and run the command `$(export cat .env)`. Happy hacking! + + -- cgit v1.2.3 From bfc9327a17322d1870c5cb51cb89390d0d4f8ebd Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Thu, 11 Apr 2019 20:21:40 +0200 Subject: Remove `ENGINE` variable export documentation. --- docs/setup.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/setup.md b/docs/setup.md index dfaf3122..2f791923 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -26,8 +26,7 @@ in your environment variables: export DATABASE_URL=postgres://pysite@localhost/pysite ``` -After this step, inside the `.env` file, set the `SECRET_KEY` variable which can be anything you like. Also, set the `ENGINE` variable -to `django.db.backends.postgresql`. +After this step, inside the `.env` file, set the `SECRET_KEY` variable which can be anything you like. A simpler approach to automatically configuring this might come in the near future - if you have any suggestions, please let us know! -- cgit v1.2.3 From aa8f0855e2a44566b79f178b489cf52239ad8b82 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Thu, 11 Apr 2019 20:23:38 +0200 Subject: Document `LOG_LEVEL` and `DEBUG` setup. --- docs/setup.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/setup.md b/docs/setup.md index 2f791923..18f5ee97 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -68,8 +68,7 @@ pip install -e .[lint,test] to install base dependencies along with lint and test dependencies. -You can either use `python manage.py` directly, or you can use the console -entrypoint for it, `psmgr`. For example, to run tests, you could use either `python manage.py test` or `psmgr test`. +To run tests, use `python manage.py test`. ## Hosts file @@ -85,8 +84,10 @@ Make sure you add the following to your hosts file: ``` When trying to access the site, you'll be using the domains above instead of the usual `localhost:8000`. -To run the server, run `DEBUG=1 python manage.py runserver`. If it gives you an error saying +Finally, you will need to set the environment variable `DEBUG=1`. When using `pipenv`, you can +set put this into an `.env` file to have it exported automatically. It's also recommended to +export `LOG_LEVEL=INFO` when using `DEBUG=1` if you don't want super verbose logs. + +To run the server, run `python manage.py runserver`. If it gives you an error saying `django.core.exceptions.ImproperlyConfigured: Set the DATABASE_URL environment variable` please make sure the server that your postgres database is located at is running and run the command `$(export cat .env)`. Happy hacking! - - -- cgit v1.2.3 From 5f5ff88445e0b8922553345520b39bd47b0df979 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Thu, 11 Apr 2019 20:28:19 +0200 Subject: Use `set -eux` in pipeline script. --- azure-pipelines.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 78bbffae..35fb9f82 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -8,8 +8,10 @@ jobs: steps: - script: | + set -eux + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - - sudo add-apt-repository deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable + sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" sudo apt-get update sudo apt-get install docker-ce displayName: install docker -- cgit v1.2.3 From c6d78dcf0781ed3dbd7f063f0c12ac5b94352f0d Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Thu, 11 Apr 2019 20:42:31 +0200 Subject: Remove `--dev` from container installation. --- docker/app/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/app/Dockerfile b/docker/app/Dockerfile index b12da08f..5b965740 100644 --- a/docker/app/Dockerfile +++ b/docker/app/Dockerfile @@ -28,7 +28,7 @@ RUN rm -r /opt/bitnami/python/lib/python3.*/site-packages/setuptools* && \ pip install --no-cache-dir -U setuptools RUN python3 -m pip install pipenv \ - && python3 -m pipenv install --dev --system --deploy \ + && python3 -m pipenv install --system --deploy \ && pip install uwsgi==2.0.18 COPY . . -- cgit v1.2.3 From dfd151f962fdd4da62ccd49cf3010c1a24cde5dd Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Thu, 11 Apr 2019 20:47:36 +0200 Subject: Ignore pip version pinning complaints. --- .hadolint.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.hadolint.yaml b/.hadolint.yaml index 5a0a0197..c3c3449b 100644 --- a/.hadolint.yaml +++ b/.hadolint.yaml @@ -1,4 +1,4 @@ ignored: - # Ignore suggestion for pinned versions in `apt-get´ and `apk` - - DL3008 - - DL3018 + - DL3008 # Ignore suggestion for pinned versions in `apt-get`... + - DL3013 # ... and `pip`. + - DL3018 # ... and `apk`. -- cgit v1.2.3 From b84ebf6e1c813f7973f3eeceafb5c8de4769be3b Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 14 Apr 2019 21:57:45 +0200 Subject: Fixup broken reverse in test. --- pydis_site/apps/home/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/apps/home/tests.py b/pydis_site/apps/home/tests.py index 54fac6e8..d1d888b4 100644 --- a/pydis_site/apps/home/tests.py +++ b/pydis_site/apps/home/tests.py @@ -4,6 +4,6 @@ from django_hosts.resolvers import reverse 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) -- cgit v1.2.3 From ba99fac19d1c4ffff453fba3e3ba273825a991fe Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 14 Apr 2019 22:00:27 +0200 Subject: Add missing test. --- pydis_site/apps/home/tests.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pydis_site/apps/home/tests.py b/pydis_site/apps/home/tests.py index d1d888b4..733ddaa3 100644 --- a/pydis_site/apps/home/tests.py +++ b/pydis_site/apps/home/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('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')) -- cgit v1.2.3 From 5002b44c76ed66d9d1ed898b302e489473b143d0 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 14 Apr 2019 21:52:41 +0200 Subject: Move models to submodules. --- .../api/migrations/0008_tag_embed_validator.py | 4 +- .../apps/api/migrations/0019_deletedmessage.py | 3 +- .../0034_add_botsetting_name_validator.py | 4 +- pydis_site/apps/api/models.py | 498 --------------------- pydis_site/apps/api/models/__init__.py | 20 + pydis_site/apps/api/models/bot/__init__.py | 16 + pydis_site/apps/api/models/bot/bot_setting.py | 27 ++ pydis_site/apps/api/models/bot/deleted_message.py | 12 + .../apps/api/models/bot/documentation_link.py | 25 ++ pydis_site/apps/api/models/bot/infraction 3.py | 67 +++ pydis_site/apps/api/models/bot/infraction.py | 67 +++ pydis_site/apps/api/models/bot/message.py | 51 +++ .../api/models/bot/message_deletion_context.py | 23 + pydis_site/apps/api/models/bot/nomination.py | 33 ++ .../apps/api/models/bot/off_topic_channel_name.py | 16 + pydis_site/apps/api/models/bot/reminder.py | 44 ++ pydis_site/apps/api/models/bot/role.py | 48 ++ pydis_site/apps/api/models/bot/snake_fact.py | 16 + pydis_site/apps/api/models/bot/snake_idiom.py | 16 + pydis_site/apps/api/models/bot/snake_name.py | 23 + pydis_site/apps/api/models/bot/special_snake.py | 26 ++ pydis_site/apps/api/models/bot/tag.py | 179 ++++++++ pydis_site/apps/api/models/bot/user.py | 52 +++ pydis_site/apps/api/models/log_entry.py | 50 +++ pydis_site/apps/api/models/utils.py | 20 + pydis_site/apps/api/tests/test_validators.py | 6 +- pydis_site/apps/api/validators.py | 164 ------- 27 files changed, 838 insertions(+), 672 deletions(-) delete mode 100644 pydis_site/apps/api/models.py create mode 100644 pydis_site/apps/api/models/__init__.py create mode 100644 pydis_site/apps/api/models/bot/__init__.py create mode 100644 pydis_site/apps/api/models/bot/bot_setting.py create mode 100644 pydis_site/apps/api/models/bot/deleted_message.py create mode 100644 pydis_site/apps/api/models/bot/documentation_link.py create mode 100644 pydis_site/apps/api/models/bot/infraction 3.py create mode 100644 pydis_site/apps/api/models/bot/infraction.py create mode 100644 pydis_site/apps/api/models/bot/message.py create mode 100644 pydis_site/apps/api/models/bot/message_deletion_context.py create mode 100644 pydis_site/apps/api/models/bot/nomination.py create mode 100644 pydis_site/apps/api/models/bot/off_topic_channel_name.py create mode 100644 pydis_site/apps/api/models/bot/reminder.py create mode 100644 pydis_site/apps/api/models/bot/role.py create mode 100644 pydis_site/apps/api/models/bot/snake_fact.py create mode 100644 pydis_site/apps/api/models/bot/snake_idiom.py create mode 100644 pydis_site/apps/api/models/bot/snake_name.py create mode 100644 pydis_site/apps/api/models/bot/special_snake.py create mode 100644 pydis_site/apps/api/models/bot/tag.py create mode 100644 pydis_site/apps/api/models/bot/user.py create mode 100644 pydis_site/apps/api/models/log_entry.py create mode 100644 pydis_site/apps/api/models/utils.py delete mode 100644 pydis_site/apps/api/validators.py 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/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})>' 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/validators.py b/pydis_site/apps/api/validators.py deleted file mode 100644 index ea2112a9..00000000 --- a/pydis_site/apps/api/validators.py +++ /dev/null @@ -1,164 +0,0 @@ -from collections.abc import Mapping - -from django.core.exceptions import ValidationError -from django.core.validators import MaxLengthValidator, MinLengthValidator - - -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) - - -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.") -- cgit v1.2.3 From 930af797bfe7405d3543c9b408682b772534c2b7 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 14 Apr 2019 22:33:55 +0200 Subject: Remove the third infraction. --- pydis_site/apps/api/models/bot/infraction 3.py | 67 -------------------------- 1 file changed, 67 deletions(-) delete mode 100644 pydis_site/apps/api/models/bot/infraction 3.py diff --git a/pydis_site/apps/api/models/bot/infraction 3.py b/pydis_site/apps/api/models/bot/infraction 3.py deleted file mode 100644 index 76d7b881..00000000 --- a/pydis_site/apps/api/models/bot/infraction 3.py +++ /dev/null @@ -1,67 +0,0 @@ -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 -- cgit v1.2.3 From 7c6ee82ed6f90121c163f7f684a289078933e2e3 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Wed, 17 Apr 2019 21:39:12 +0200 Subject: Restructure viewsets to submodules. --- pydis_site/apps/api/models/bot/message.py | 2 +- pydis_site/apps/api/viewsets.py | 923 --------------------- pydis_site/apps/api/viewsets/__init__.py | 17 + pydis_site/apps/api/viewsets/bot/__init__.py | 14 + pydis_site/apps/api/viewsets/bot/bot_setting.py | 14 + .../apps/api/viewsets/bot/deleted_message.py | 40 + .../apps/api/viewsets/bot/documentation_link.py | 72 ++ pydis_site/apps/api/viewsets/bot/infraction.py | 155 ++++ pydis_site/apps/api/viewsets/bot/nomination.py | 25 + .../api/viewsets/bot/off_topic_channel_name.py | 98 +++ pydis_site/apps/api/viewsets/bot/reminder.py | 66 ++ pydis_site/apps/api/viewsets/bot/role.py | 95 +++ pydis_site/apps/api/viewsets/bot/snake_fact.py | 30 + pydis_site/apps/api/viewsets/bot/snake_idiom.py | 30 + pydis_site/apps/api/viewsets/bot/snake_name.py | 61 ++ pydis_site/apps/api/viewsets/bot/special_snake.py | 33 + pydis_site/apps/api/viewsets/bot/tag.py | 105 +++ pydis_site/apps/api/viewsets/bot/user.py | 126 +++ pydis_site/apps/api/viewsets/log_entry.py | 37 + 19 files changed, 1019 insertions(+), 924 deletions(-) delete mode 100644 pydis_site/apps/api/viewsets.py create mode 100644 pydis_site/apps/api/viewsets/__init__.py create mode 100644 pydis_site/apps/api/viewsets/bot/__init__.py create mode 100644 pydis_site/apps/api/viewsets/bot/bot_setting.py create mode 100644 pydis_site/apps/api/viewsets/bot/deleted_message.py create mode 100644 pydis_site/apps/api/viewsets/bot/documentation_link.py create mode 100644 pydis_site/apps/api/viewsets/bot/infraction.py create mode 100644 pydis_site/apps/api/viewsets/bot/nomination.py create mode 100644 pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py create mode 100644 pydis_site/apps/api/viewsets/bot/reminder.py create mode 100644 pydis_site/apps/api/viewsets/bot/role.py create mode 100644 pydis_site/apps/api/viewsets/bot/snake_fact.py create mode 100644 pydis_site/apps/api/viewsets/bot/snake_idiom.py create mode 100644 pydis_site/apps/api/viewsets/bot/snake_name.py create mode 100644 pydis_site/apps/api/viewsets/bot/special_snake.py create mode 100644 pydis_site/apps/api/viewsets/bot/tag.py create mode 100644 pydis_site/apps/api/viewsets/bot/user.py create mode 100644 pydis_site/apps/api/viewsets/log_entry.py diff --git a/pydis_site/apps/api/models/bot/message.py b/pydis_site/apps/api/models/bot/message.py index 22500be0..2578d5c6 100644 --- a/pydis_site/apps/api/models/bot/message.py +++ b/pydis_site/apps/api/models/bot/message.py @@ -2,9 +2,9 @@ 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 +from pydis_site.apps.api.models.utils import ModelReprMixin class Message(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/viewsets.py b/pydis_site/apps/api/viewsets.py deleted file mode 100644 index 47915256..00000000 --- a/pydis_site/apps/api/viewsets.py +++ /dev/null @@ -1,923 +0,0 @@ -from django.shortcuts import get_object_or_404 -from django_filters.rest_framework import DjangoFilterBackend -from rest_framework.decorators import action -from rest_framework.exceptions import ParseError, ValidationError -from rest_framework.filters import SearchFilter -from rest_framework.mixins import ( - CreateModelMixin, DestroyModelMixin, - ListModelMixin, RetrieveModelMixin, - UpdateModelMixin -) -from rest_framework.response import Response -from rest_framework.status import HTTP_201_CREATED -from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet -from rest_framework_bulk import BulkCreateModelMixin - -from .models import ( - BotSetting, DocumentationLink, - Infraction, LogEntry, - MessageDeletionContext, Nomination, - OffTopicChannelName, Reminder, - Role, SnakeFact, - SnakeIdiom, SnakeName, - SpecialSnake, Tag, - User -) -from .serializers import ( - BotSettingSerializer, DocumentationLinkSerializer, - ExpandedInfractionSerializer, InfractionSerializer, - LogEntrySerializer, MessageDeletionContextSerializer, - NominationSerializer, OffTopicChannelNameSerializer, - ReminderSerializer, RoleSerializer, - SnakeFactSerializer, SnakeIdiomSerializer, - SnakeNameSerializer, SpecialSnakeSerializer, - TagSerializer, UserSerializer -) - - -class BotSettingViewSet(RetrieveModelMixin, UpdateModelMixin, GenericViewSet): - """ - View providing update operations on bot setting routes. - """ - - serializer_class = BotSettingSerializer - queryset = BotSetting.objects.all() - - -class DeletedMessageViewSet(CreateModelMixin, GenericViewSet): - """ - View providing support for posting bulk deletion logs generated by the bot. - - ## Routes - ### POST /bot/deleted-messages - Post messages from bulk deletion logs. - - #### Body schema - >>> { - ... # The member ID of the original actor, if applicable. - ... # If a member ID is given, it must be present on the site. - ... 'actor': Optional[int] - ... 'creation': datetime, - ... 'messages': [ - ... { - ... 'id': int, - ... 'author': int, - ... 'channel_id': int, - ... 'content': str, - ... 'embeds': [ - ... # Discord embed objects - ... ] - ... } - ... ] - ... } - - #### Status codes - - 204: returned on success - """ - - queryset = MessageDeletionContext.objects.all() - serializer_class = MessageDeletionContextSerializer - - -class DocumentationLinkViewSet( - CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin, GenericViewSet -): - """ - View providing management of documentation links used in the bot's `Doc` cog. - - ## Routes - ### GET /bot/documentation-links - Retrieve all currently stored entries from the database. - - #### Response format - >>> [ - ... { - ... 'package': 'flask', - ... 'base_url': 'https://flask.pocoo.org/docs/dev', - ... 'inventory_url': 'https://flask.pocoo.org/docs/objects.inv' - ... }, - ... # ... - ... ] - - #### Status codes - - 200: returned on success - - ### GET /bot/documentation-links/ - Look up the documentation object for the given `package`. - - #### Response format - >>> { - ... 'package': 'flask', - ... 'base_url': 'https://flask.pocoo.org/docs/dev', - ... 'inventory_url': 'https://flask.pocoo.org/docs/objects.inv' - ... } - - #### Status codes - - 200: returned on success - - 404: if no entry for the given `package` exists - - ### POST /bot/documentation-links - Create a new documentation link object. - - #### Body schema - >>> { - ... 'package': str, - ... 'base_url': URL, - ... 'inventory_url': URL - ... } - - #### Status codes - - 201: returned on success - - 400: if the request body has invalid fields, see the response for details - - ### DELETE /bot/documentation-links/ - Delete the entry for the given `package`. - - #### Status codes - - 204: returned on success - - 404: if the given `package` could not be found - """ - - queryset = DocumentationLink.objects.all() - serializer_class = DocumentationLinkSerializer - lookup_field = 'package' - - -class InfractionViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, GenericViewSet): - """ - View providing CRUD operations on infractions for Discord users. - - ## Routes - ### GET /bot/infractions - Retrieve all infractions. - May be filtered by the query parameters. - - #### Query parameters - - **active** `bool`: whether the infraction is still active - - **actor** `int`: snowflake of the user which applied the infraction - - **hidden** `bool`: whether the infraction is a shadow infraction - - **search** `str`: regular expression applied to the infraction's reason - - **type** `str`: the type of the infraction - - **user** `int`: snowflake of the user to which the infraction was applied - - Invalid query parameters are ignored. - - #### Response format - >>> [ - ... { - ... 'id': 5, - ... 'inserted_at': '2018-11-22T07:24:06.132307Z', - ... 'expires_at': '5018-11-20T15:52:00Z', - ... 'active': False, - ... 'user': 172395097705414656, - ... 'actor': 125435062127820800, - ... 'type': 'ban', - ... 'reason': 'He terk my jerb!', - ... 'hidden': True - ... } - ... ] - - #### Status codes - - 200: returned on success - - ### GET /bot/infractions/ - Retrieve a single infraction by ID. - - #### Response format - See `GET /bot/infractions`. - - #### Status codes - - 200: returned on success - - 404: if an infraction with the given `id` could not be found - - ### POST /bot/infractions - Create a new infraction and return the created infraction. - Only `actor`, `type`, and `user` are required. - The `actor` and `user` must be users known by the site. - - #### Request body - >>> { - ... 'active': False, - ... 'actor': 125435062127820800, - ... 'expires_at': '5018-11-20T15:52:00+00:00', - ... 'hidden': True, - ... 'type': 'ban', - ... 'reason': 'He terk my jerb!', - ... 'user': 172395097705414656 - ... } - - #### Response format - See `GET /bot/infractions`. - - #### Status codes - - 201: returned on success - - 400: if a given user is unknown or a field in the request body is invalid - - ### PATCH /bot/infractions/ - Update the infraction with the given `id` and return the updated infraction. - Only `active`, `reason`, and `expires_at` may be updated. - - #### Request body - >>> { - ... 'active': True, - ... 'expires_at': '4143-02-15T21:04:31+00:00', - ... 'reason': 'durka derr' - ... } - - #### Response format - See `GET /bot/infractions`. - - #### Status codes - - 200: returned on success - - 400: if a field in the request body is invalid or disallowed - - 404: if an infraction with the given `id` could not be found - - ### Expanded routes - All routes support expansion of `user` and `actor` in responses. To use an expanded route, - append `/expanded` to the end of the route e.g. `GET /bot/infractions/expanded`. - - #### Response format - See `GET /bot/users/` for the expanded formats of `user` and `actor`. Responses - are otherwise identical to their non-expanded counterparts. - """ - - serializer_class = InfractionSerializer - queryset = Infraction.objects.all() - filter_backends = (DjangoFilterBackend, SearchFilter) - filter_fields = ('user__id', 'actor__id', 'active', 'hidden', 'type') - search_fields = ('$reason',) - frozen_fields = ('id', 'inserted_at', 'type', 'user', 'actor', 'hidden') - - def partial_update(self, request, *args, **kwargs): - for field in request.data: - if field in self.frozen_fields: - raise ValidationError({field: ['This field cannot be updated.']}) - - instance = self.get_object() - serializer = self.get_serializer(instance, data=request.data, partial=True) - serializer.is_valid(raise_exception=True) - serializer.save() - - return Response(serializer.data) - - @action(url_path='expanded', detail=False) - def list_expanded(self, *args, **kwargs): - self.serializer_class = ExpandedInfractionSerializer - return self.list(*args, **kwargs) - - @list_expanded.mapping.post - def create_expanded(self, *args, **kwargs): - self.serializer_class = ExpandedInfractionSerializer - return self.create(*args, **kwargs) - - @action(url_path='expanded', url_name='detail-expanded', detail=True) - def retrieve_expanded(self, *args, **kwargs): - self.serializer_class = ExpandedInfractionSerializer - return self.retrieve(*args, **kwargs) - - @retrieve_expanded.mapping.patch - def partial_update_expanded(self, *args, **kwargs): - self.serializer_class = ExpandedInfractionSerializer - 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 - to rotate our off-topic names on a daily basis. - - ## Routes - ### GET /bot/off-topic-channel-names - Return all known off-topic channel names from the database. - If the `random_items` query parameter is given, for example using... - $ curl api.pythondiscord.local:8000/bot/off-topic-channel-names?random_items=5 - ... then the API will return `5` random items from the database. - - #### Response format - Return a list of off-topic-channel names: - >>> [ - ... "lemons-lemonade-stand", - ... "bbq-with-bisk" - ... ] - - #### Status codes - - 200: returned on success - - 400: returned when `random_items` is not a positive integer - - ### POST /bot/off-topic-channel-names - Create a new off-topic-channel name in the database. - The name must be given as a query parameter, for example: - $ curl api.pythondiscord.local:8000/bot/off-topic-channel-names?name=lemons-lemonade-shop - - #### Status codes - - 201: returned on success - - 400: if the request body has invalid fields, see the response for details - - ### DELETE /bot/off-topic-channel-names/ - Delete the off-topic-channel name with the given `name`. - - #### Status codes - - 204: returned on success - - 404: returned when the given `name` was not found - - ## Authentication - Requires a API token. - """ - - lookup_field = 'name' - serializer_class = OffTopicChannelNameSerializer - - def get_object(self): - queryset = self.get_queryset() - name = self.kwargs[self.lookup_field] - return get_object_or_404(queryset, name=name) - - def get_queryset(self): - return OffTopicChannelName.objects.all() - - def create(self, request): - if 'name' in request.query_params: - create_data = {'name': request.query_params['name']} - serializer = OffTopicChannelNameSerializer(data=create_data) - serializer.is_valid(raise_exception=True) - serializer.save() - return Response(create_data, status=HTTP_201_CREATED) - - else: - raise ParseError(detail={ - 'name': ["This query parameter is required."] - }) - - def list(self, request): # noqa - if 'random_items' in request.query_params: - param = request.query_params['random_items'] - try: - random_count = int(param) - except ValueError: - raise ParseError(detail={'random_items': ["Must be a valid integer."]}) - - if random_count <= 0: - raise ParseError(detail={ - 'random_items': ["Must be a positive integer."] - }) - - queryset = self.get_queryset().order_by('?')[:random_count] - serialized = self.serializer_class(queryset, many=True) - return Response(serialized.data) - - queryset = self.get_queryset() - serialized = self.serializer_class(queryset, many=True) - return Response(serialized.data) - - -class ReminderViewSet(CreateModelMixin, ListModelMixin, DestroyModelMixin, GenericViewSet): - """ - View providing CRUD access to reminders. - - ## Routes - ### GET /bot/reminders - Returns all reminders in the database. - - #### Response format - >>> [ - ... { - ... 'active': True, - ... 'author': 1020103901030, - ... 'content': "Make dinner", - ... 'expiration': '5018-11-20T15:52:00Z', - ... 'id': 11 - ... }, - ... ... - ... ] - - #### Status codes - - 200: returned on success - - ### POST /bot/reminders - Create a new reminder. - - #### Request body - >>> { - ... 'author': int, - ... 'content': str, - ... 'expiration': str # ISO-formatted datetime - ... } - - #### Status codes - - 201: returned on success - - 400: if the body format is invalid - - 404: if no user with the given ID could be found - - ### DELETE /bot/reminders/ - Delete the reminder with the given `id`. - - #### Status codes - - 204: returned on success - - 404: if a reminder with the given `id` does not exist - - ## Authentication - Requires an API token. - """ - - serializer_class = ReminderSerializer - queryset = Reminder.objects.prefetch_related('author') - filter_backends = (DjangoFilterBackend, SearchFilter) - filter_fields = ('active', 'author__id') - - -class RoleViewSet(ModelViewSet): - """ - View providing CRUD access to the roles on our server, used - by the bot to keep a mirror of our server's roles on the site. - - ## Routes - ### GET /bot/roles - Returns all roles in the database. - - #### Response format - >>> [ - ... { - ... 'id': 267628507062992896, - ... 'name': "Admins", - ... 'colour': 1337, - ... 'permissions': 8 - ... } - ... ] - - #### Status codes - - 200: returned on success - - ### GET /bot/roles/ - Gets a single role by ID. - - #### Response format - >>> { - ... 'id': 267628507062992896, - ... 'name': "Admins", - ... 'colour': 1337, - ... 'permissions': 8 - ... } - - #### Status codes - - 200: returned on success - - 404: if a role with the given `snowflake` could not be found - - ### POST /bot/roles - Adds a single new role. - - #### Request body - >>> { - ... 'id': int, - ... 'name': str, - ... 'colour': int, - ... 'permissions': int, - ... } - - #### Status codes - - 201: returned on success - - 400: if the body format is invalid - - ### PUT /bot/roles/ - Update the role with the given `snowflake`. - All fields in the request body are required. - - #### Request body - >>> { - ... 'id': int, - ... 'name': str, - ... 'colour': int, - ... 'permissions': int - ... } - - #### Status codes - - 200: returned on success - - 400: if the request body was invalid - - ### PATCH /bot/roles/ - Update the role with the given `snowflake`. - All fields in the request body are required. - - >>> { - ... 'id': int, - ... 'name': str, - ... 'colour': int, - ... 'permissions': int - ... } - - ### DELETE /bot/roles/ - Deletes the role with the given `snowflake`. - - #### Status codes - - 204: returned on success - - 404: if a role with the given `snowflake` does not exist - """ - - queryset = Role.objects.all() - serializer_class = RoleSerializer - - -class SnakeFactViewSet(ListModelMixin, GenericViewSet): - """ - View providing snake facts created by the Pydis community in the first code jam. - - ## Routes - ### GET /bot/snake-facts - Returns snake facts from the database. - - #### Response format - >>> [ - ... {'fact': 'Snakes are dangerous'}, - ... {'fact': 'Except for Python, we all love it'} - ... ] - - #### Status codes - - 200: returned on success - - ## Authentication - Requires an API token. - """ - - serializer_class = SnakeFactSerializer - queryset = SnakeFact.objects.all() - - -class SnakeIdiomViewSet(ListModelMixin, GenericViewSet): - """ - View providing snake idioms for the snake cog. - - ## Routes - ### GET /bot/snake-idioms - Returns snake idioms from the database. - - #### Response format - >>> [ - ... {'idiom': 'Sneky snek'}, - ... {'idiom': 'Snooky Snake'} - ... ] - - #### Status codes - - 200: returned on success - - ## Authentication - Requires an API token - """ - - serializer_class = SnakeIdiomSerializer - queryset = SnakeIdiom.objects.all() - - -class SnakeNameViewSet(ViewSet): - """ - View providing snake names for the bot's snake cog from our first code jam's winners. - - ## Routes - ### GET /bot/snake-names - By default, return a single random snake name along with its name and scientific name. - If the `get_all` query parameter is given, for example using... - $ curl api.pythondiscord.local:8000/bot/snake-names?get_all=yes - ... then the API will return all snake names and scientific names in the database. - - #### Response format - Without `get_all` query parameter: - >>> { - ... 'name': "Python", - ... 'scientific': "Langus greatus" - ... } - - If the database is empty for whatever reason, this will return an empty dictionary. - - With `get_all` query parameter: - >>> [ - ... {'name': "Python 3", 'scientific': "Langus greatus"}, - ... {'name': "Python 2", 'scientific': "Langus decentus"} - ... ] - - #### Status codes - - 200: returned on success - - ## Authentication - Requires a API token. - """ - - serializer_class = SnakeNameSerializer - - def get_queryset(self): - return SnakeName.objects.all() - - def list(self, request): # noqa - if request.query_params.get('get_all'): - queryset = self.get_queryset() - serialized = self.serializer_class(queryset, many=True) - return Response(serialized.data) - - single_snake = SnakeName.objects.order_by('?').first() - if single_snake is not None: - body = { - 'name': single_snake.name, - 'scientific': single_snake.scientific - } - - return Response(body) - - return Response({}) - - -class SpecialSnakeViewSet(ListModelMixin, GenericViewSet): - """ - View providing special snake names for our bot's snake cog. - - ## Routes - ### GET /bot/special-snakes - Returns a list of special snake names. - - #### Response Format - >>> [ - ... { - ... 'name': 'Snakky sneakatus', - ... 'info': 'Scary snek', - ... 'image': 'https://discordapp.com/assets/53ef346458017da2062aca5c7955946b.svg' - ... } - ... ] - - #### Status codes - - 200: returned on success - - ## Authentication - Requires an API token. - """ - - serializer_class = SpecialSnakeSerializer - queryset = SpecialSnake.objects.all() - - -class TagViewSet(ModelViewSet): - """ - View providing CRUD operations on tags shown by our bot. - - ## Routes - ### GET /bot/tags - Returns all tags in the database. - - #### Response format - >>> [ - ... { - ... 'title': "resources", - ... 'embed': { - ... 'content': "Did you really think I'd put something useful here?" - ... } - ... } - ... ] - - #### Status codes - - 200: returned on success - - ### GET /bot/tags/ - Gets a single tag by its title. - - #### Response format - >>> { - ... 'title': "My awesome tag", - ... 'embed': { - ... 'content': "totally not filler words" - ... } - ... } - - #### Status codes - - 200: returned on success - - 404: if a tag with the given `title` could not be found - - ### POST /bot/tags - Adds a single tag to the database. - - #### Request body - >>> { - ... 'title': str, - ... 'embed': dict - ... } - - The embed structure is the same as the embed structure that the Discord API - expects. You can view the documentation for it here: - https://discordapp.com/developers/docs/resources/channel#embed-object - - #### Status codes - - 201: returned on success - - 400: if one of the given fields is invalid - - ### PUT /bot/tags/ - Update the tag with the given `title`. - - #### Request body - >>> { - ... 'title': str, - ... 'embed': dict - ... } - - The embed structure is the same as the embed structure that the Discord API - expects. You can view the documentation for it here: - https://discordapp.com/developers/docs/resources/channel#embed-object - - #### Status codes - - 200: returned on success - - 400: if the request body was invalid, see response body for details - - 404: if the tag with the given `title` could not be found - - ### PATCH /bot/tags/ - Update the tag with the given `title`. - - #### Request body - >>> { - ... 'title': str, - ... 'embed': dict - ... } - - The embed structure is the same as the embed structure that the Discord API - expects. You can view the documentation for it here: - https://discordapp.com/developers/docs/resources/channel#embed-object - - #### Status codes - - 200: returned on success - - 400: if the request body was invalid, see response body for details - - 404: if the tag with the given `title` could not be found - - ### DELETE /bot/tags/ - Deletes the tag with the given `title`. - - #### Status codes - - 204: returned on success - - 404: if a tag with the given `title` does not exist - """ - - serializer_class = TagSerializer - queryset = Tag.objects.all() - - -class UserViewSet(BulkCreateModelMixin, ModelViewSet): - """ - View providing CRUD operations on Discord users through the bot. - - ## Routes - ### GET /bot/users - Returns all users currently known. - - #### Response format - >>> [ - ... { - ... 'id': 409107086526644234, - ... 'avatar': "3ba3c1acce584c20b1e96fc04bbe80eb", - ... 'name': "Python", - ... 'discriminator': 4329, - ... 'roles': [ - ... 352427296948486144, - ... 270988689419665409, - ... 277546923144249364, - ... 458226699344019457 - ... ], - ... 'in_guild': True - ... } - ... ] - - #### Status codes - - 200: returned on success - - ### GET /bot/users/ - Gets a single user by ID. - - #### Response format - >>> { - ... 'id': 409107086526644234, - ... 'avatar': "3ba3c1acce584c20b1e96fc04bbe80eb", - ... 'name': "Python", - ... 'discriminator': 4329, - ... 'roles': [ - ... 352427296948486144, - ... 270988689419665409, - ... 277546923144249364, - ... 458226699344019457 - ... ], - ... 'in_guild': True - ... } - - #### Status codes - - 200: returned on success - - 404: if a user with the given `snowflake` could not be found - - ### POST /bot/users - Adds a single or multiple new users. - The roles attached to the user(s) must be roles known by the site. - - #### Request body - >>> { - ... 'id': int, - ... 'avatar': str, - ... 'name': str, - ... 'discriminator': int, - ... 'roles': List[int], - ... 'in_guild': bool - ... } - - Alternatively, request users can be POSTed as a list of above objects, - in which case multiple users will be created at once. - - #### Status codes - - 201: returned on success - - 400: if one of the given roles does not exist, or one of the given fields is invalid - - ### PUT /bot/users/ - Update the user with the given `snowflake`. - All fields in the request body are required. - - #### Request body - >>> { - ... 'id': int, - ... 'avatar': str, - ... 'name': str, - ... 'discriminator': int, - ... 'roles': List[int], - ... 'in_guild': bool - ... } - - #### Status codes - - 200: returned on success - - 400: if the request body was invalid, see response body for details - - 404: if the user with the given `snowflake` could not be found - - ### PATCH /bot/users/ - Update the user with the given `snowflake`. - All fields in the request body are optional. - - #### Request body - >>> { - ... 'id': int, - ... 'avatar': str, - ... 'name': str, - ... 'discriminator': int, - ... 'roles': List[int], - ... 'in_guild': bool - ... } - - #### Status codes - - 200: returned on success - - 400: if the request body was invalid, see response body for details - - 404: if the user with the given `snowflake` could not be found - - ### DELETE /bot/users/ - Deletes the user with the given `snowflake`. - - #### Status codes - - 204: returned on success - - 404: if a user with the given `snowflake` does not exist - """ - - serializer_class = UserSerializer - queryset = User.objects.prefetch_related('roles') - - -class NominationViewSet(ModelViewSet): - # TODO: doc me - serializer_class = NominationSerializer - queryset = Nomination.objects.prefetch_related('author', 'user') - frozen_fields = ('author', 'inserted_at', 'user') - - def update(self, request, *args, **kwargs): - for field in request.data: - if field in self.frozen_fields: - raise ValidationError({field: ['This field cannot be updated.']}) - - instance = self.get_object() - serializer = self.get_serializer(instance, data=request.data, partial=True) - serializer.is_valid(raise_exception=True) - serializer.save() - - return Response(serializer.data) diff --git a/pydis_site/apps/api/viewsets/__init__.py b/pydis_site/apps/api/viewsets/__init__.py new file mode 100644 index 00000000..553ca2c3 --- /dev/null +++ b/pydis_site/apps/api/viewsets/__init__.py @@ -0,0 +1,17 @@ +from .bot import ( # noqa + BotSettingViewSet, + DeletedMessageViewSet, + DocumentationLinkViewSet, + InfractionViewSet, + NominationViewSet, + OffTopicChannelNameViewSet, + ReminderViewSet, + RoleViewSet, + SnakeFactViewSet, + SnakeIdiomViewSet, + SnakeNameViewSet, + SpecialSnakeViewSet, + TagViewSet, + UserViewSet +) +from .log_entry import LogEntryViewSet # noqa diff --git a/pydis_site/apps/api/viewsets/bot/__init__.py b/pydis_site/apps/api/viewsets/bot/__init__.py new file mode 100644 index 00000000..8e7d1290 --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/__init__.py @@ -0,0 +1,14 @@ +from .bot_setting import BotSettingViewSet # noqa +from .deleted_message import DeletedMessageViewSet # noqa +from .documentation_link import DocumentationLinkViewSet # noqa +from .infraction import InfractionViewSet # noqa +from .nomination import NominationViewSet # noqa +from .off_topic_channel_name import OffTopicChannelNameViewSet # noqa +from .reminder import ReminderViewSet # noqa +from .role import RoleViewSet # noqa +from .snake_fact import SnakeFactViewSet # noqa +from .snake_idiom import SnakeIdiomViewSet # noqa +from .snake_name import SnakeNameViewSet # noqa +from .special_snake import SpecialSnakeViewSet # noqa +from .tag import TagViewSet # noqa +from .user import UserViewSet # noqa diff --git a/pydis_site/apps/api/viewsets/bot/bot_setting.py b/pydis_site/apps/api/viewsets/bot/bot_setting.py new file mode 100644 index 00000000..5464018a --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/bot_setting.py @@ -0,0 +1,14 @@ +from rest_framework.mixins import RetrieveModelMixin, UpdateModelMixin +from rest_framework.viewsets import GenericViewSet + +from pydis_site.apps.api.models.bot.bot_setting import BotSetting +from pydis_site.apps.api.serializers import BotSettingSerializer + + +class BotSettingViewSet(RetrieveModelMixin, UpdateModelMixin, GenericViewSet): + """ + View providing update operations on bot setting routes. + """ + + serializer_class = BotSettingSerializer + queryset = BotSetting.objects.all() diff --git a/pydis_site/apps/api/viewsets/bot/deleted_message.py b/pydis_site/apps/api/viewsets/bot/deleted_message.py new file mode 100644 index 00000000..c14171bd --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/deleted_message.py @@ -0,0 +1,40 @@ +from rest_framework.mixins import CreateModelMixin +from rest_framework.viewsets import GenericViewSet + +from pydis_site.apps.api.models.bot.message_deletion_context import MessageDeletionContext +from pydis_site.apps.api.serializers import MessageDeletionContextSerializer + + +class DeletedMessageViewSet(CreateModelMixin, GenericViewSet): + """ + View providing support for posting bulk deletion logs generated by the bot. + + ## Routes + ### POST /bot/deleted-messages + Post messages from bulk deletion logs. + + #### Body schema + >>> { + ... # The member ID of the original actor, if applicable. + ... # If a member ID is given, it must be present on the site. + ... 'actor': Optional[int] + ... 'creation': datetime, + ... 'messages': [ + ... { + ... 'id': int, + ... 'author': int, + ... 'channel_id': int, + ... 'content': str, + ... 'embeds': [ + ... # Discord embed objects + ... ] + ... } + ... ] + ... } + + #### Status codes + - 204: returned on success + """ + + queryset = MessageDeletionContext.objects.all() + serializer_class = MessageDeletionContextSerializer diff --git a/pydis_site/apps/api/viewsets/bot/documentation_link.py b/pydis_site/apps/api/viewsets/bot/documentation_link.py new file mode 100644 index 00000000..6432d344 --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/documentation_link.py @@ -0,0 +1,72 @@ +from rest_framework.mixins import ( + CreateModelMixin, DestroyModelMixin, + ListModelMixin, RetrieveModelMixin +) +from rest_framework.viewsets import GenericViewSet + +from pydis_site.apps.api.models.bot.documentation_link import DocumentationLink +from pydis_site.apps.api.serializers import DocumentationLinkSerializer + + +class DocumentationLinkViewSet( + CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin, GenericViewSet +): + """ + View providing management of documentation links used in the bot's `Doc` cog. + + ## Routes + ### GET /bot/documentation-links + Retrieve all currently stored entries from the database. + + #### Response format + >>> [ + ... { + ... 'package': 'flask', + ... 'base_url': 'https://flask.pocoo.org/docs/dev', + ... 'inventory_url': 'https://flask.pocoo.org/docs/objects.inv' + ... }, + ... # ... + ... ] + + #### Status codes + - 200: returned on success + + ### GET /bot/documentation-links/ + Look up the documentation object for the given `package`. + + #### Response format + >>> { + ... 'package': 'flask', + ... 'base_url': 'https://flask.pocoo.org/docs/dev', + ... 'inventory_url': 'https://flask.pocoo.org/docs/objects.inv' + ... } + + #### Status codes + - 200: returned on success + - 404: if no entry for the given `package` exists + + ### POST /bot/documentation-links + Create a new documentation link object. + + #### Body schema + >>> { + ... 'package': str, + ... 'base_url': URL, + ... 'inventory_url': URL + ... } + + #### Status codes + - 201: returned on success + - 400: if the request body has invalid fields, see the response for details + + ### DELETE /bot/documentation-links/ + Delete the entry for the given `package`. + + #### Status codes + - 204: returned on success + - 404: if the given `package` could not be found + """ + + queryset = DocumentationLink.objects.all() + serializer_class = DocumentationLinkSerializer + lookup_field = 'package' diff --git a/pydis_site/apps/api/viewsets/bot/infraction.py b/pydis_site/apps/api/viewsets/bot/infraction.py new file mode 100644 index 00000000..8eacf5c1 --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/infraction.py @@ -0,0 +1,155 @@ +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework.decorators import action +from rest_framework.exceptions import ValidationError +from rest_framework.filters import SearchFilter +from rest_framework.mixins import ( + CreateModelMixin, + ListModelMixin, + RetrieveModelMixin +) +from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet + +from pydis_site.apps.api.models.bot.infraction import Infraction +from pydis_site.apps.api.serializers import ( + ExpandedInfractionSerializer, + InfractionSerializer +) + + +class InfractionViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, GenericViewSet): + """ + View providing CRUD operations on infractions for Discord users. + + ## Routes + ### GET /bot/infractions + Retrieve all infractions. + May be filtered by the query parameters. + + #### Query parameters + - **active** `bool`: whether the infraction is still active + - **actor** `int`: snowflake of the user which applied the infraction + - **hidden** `bool`: whether the infraction is a shadow infraction + - **search** `str`: regular expression applied to the infraction's reason + - **type** `str`: the type of the infraction + - **user** `int`: snowflake of the user to which the infraction was applied + + Invalid query parameters are ignored. + + #### Response format + >>> [ + ... { + ... 'id': 5, + ... 'inserted_at': '2018-11-22T07:24:06.132307Z', + ... 'expires_at': '5018-11-20T15:52:00Z', + ... 'active': False, + ... 'user': 172395097705414656, + ... 'actor': 125435062127820800, + ... 'type': 'ban', + ... 'reason': 'He terk my jerb!', + ... 'hidden': True + ... } + ... ] + + #### Status codes + - 200: returned on success + + ### GET /bot/infractions/ + Retrieve a single infraction by ID. + + #### Response format + See `GET /bot/infractions`. + + #### Status codes + - 200: returned on success + - 404: if an infraction with the given `id` could not be found + + ### POST /bot/infractions + Create a new infraction and return the created infraction. + Only `actor`, `type`, and `user` are required. + The `actor` and `user` must be users known by the site. + + #### Request body + >>> { + ... 'active': False, + ... 'actor': 125435062127820800, + ... 'expires_at': '5018-11-20T15:52:00+00:00', + ... 'hidden': True, + ... 'type': 'ban', + ... 'reason': 'He terk my jerb!', + ... 'user': 172395097705414656 + ... } + + #### Response format + See `GET /bot/infractions`. + + #### Status codes + - 201: returned on success + - 400: if a given user is unknown or a field in the request body is invalid + + ### PATCH /bot/infractions/ + Update the infraction with the given `id` and return the updated infraction. + Only `active`, `reason`, and `expires_at` may be updated. + + #### Request body + >>> { + ... 'active': True, + ... 'expires_at': '4143-02-15T21:04:31+00:00', + ... 'reason': 'durka derr' + ... } + + #### Response format + See `GET /bot/infractions`. + + #### Status codes + - 200: returned on success + - 400: if a field in the request body is invalid or disallowed + - 404: if an infraction with the given `id` could not be found + + ### Expanded routes + All routes support expansion of `user` and `actor` in responses. To use an expanded route, + append `/expanded` to the end of the route e.g. `GET /bot/infractions/expanded`. + + #### Response format + See `GET /bot/users/` for the expanded formats of `user` and `actor`. Responses + are otherwise identical to their non-expanded counterparts. + """ + + serializer_class = InfractionSerializer + queryset = Infraction.objects.all() + filter_backends = (DjangoFilterBackend, SearchFilter) + filter_fields = ('user__id', 'actor__id', 'active', 'hidden', 'type') + search_fields = ('$reason',) + frozen_fields = ('id', 'inserted_at', 'type', 'user', 'actor', 'hidden') + + def partial_update(self, request, *args, **kwargs): + for field in request.data: + if field in self.frozen_fields: + raise ValidationError({field: ['This field cannot be updated.']}) + + instance = self.get_object() + serializer = self.get_serializer(instance, data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + serializer.save() + + return Response(serializer.data) + + @action(url_path='expanded', detail=False) + def list_expanded(self, *args, **kwargs): + self.serializer_class = ExpandedInfractionSerializer + return self.list(*args, **kwargs) + + @list_expanded.mapping.post + def create_expanded(self, *args, **kwargs): + self.serializer_class = ExpandedInfractionSerializer + return self.create(*args, **kwargs) + + @action(url_path='expanded', url_name='detail-expanded', detail=True) + def retrieve_expanded(self, *args, **kwargs): + self.serializer_class = ExpandedInfractionSerializer + return self.retrieve(*args, **kwargs) + + @retrieve_expanded.mapping.patch + def partial_update_expanded(self, *args, **kwargs): + self.serializer_class = ExpandedInfractionSerializer + return self.partial_update(*args, **kwargs) diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py new file mode 100644 index 00000000..725ae176 --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/nomination.py @@ -0,0 +1,25 @@ +from rest_framework.exceptions import ValidationError +from rest_framework.response import Response +from rest_framework.viewsets import ModelViewSet + +from pydis_site.apps.api.models.bot import Nomination +from pydis_site.apps.api.serializers import NominationSerializer + + +class NominationViewSet(ModelViewSet): + # TODO: doc me + serializer_class = NominationSerializer + queryset = Nomination.objects.prefetch_related('author', 'user') + frozen_fields = ('author', 'inserted_at', 'user') + + def update(self, request, *args, **kwargs): + for field in request.data: + if field in self.frozen_fields: + raise ValidationError({field: ['This field cannot be updated.']}) + + instance = self.get_object() + serializer = self.get_serializer(instance, data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + serializer.save() + + return Response(serializer.data) diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py new file mode 100644 index 00000000..df51917d --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py @@ -0,0 +1,98 @@ +from django.shortcuts import get_object_or_404 +from rest_framework.exceptions import ParseError +from rest_framework.mixins import DestroyModelMixin +from rest_framework.response import Response +from rest_framework.status import HTTP_201_CREATED +from rest_framework.viewsets import ViewSet + +from pydis_site.apps.api.models.bot.off_topic_channel_name import OffTopicChannelName +from pydis_site.apps.api.serializers import OffTopicChannelNameSerializer + + +class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): + """ + View of off-topic channel names used by the bot + to rotate our off-topic names on a daily basis. + + ## Routes + ### GET /bot/off-topic-channel-names + Return all known off-topic channel names from the database. + If the `random_items` query parameter is given, for example using... + $ curl api.pythondiscord.local:8000/bot/off-topic-channel-names?random_items=5 + ... then the API will return `5` random items from the database. + + #### Response format + Return a list of off-topic-channel names: + >>> [ + ... "lemons-lemonade-stand", + ... "bbq-with-bisk" + ... ] + + #### Status codes + - 200: returned on success + - 400: returned when `random_items` is not a positive integer + + ### POST /bot/off-topic-channel-names + Create a new off-topic-channel name in the database. + The name must be given as a query parameter, for example: + $ curl api.pythondiscord.local:8000/bot/off-topic-channel-names?name=lemons-lemonade-shop + + #### Status codes + - 201: returned on success + - 400: if the request body has invalid fields, see the response for details + + ### DELETE /bot/off-topic-channel-names/ + Delete the off-topic-channel name with the given `name`. + + #### Status codes + - 204: returned on success + - 404: returned when the given `name` was not found + + ## Authentication + Requires a API token. + """ + + lookup_field = 'name' + serializer_class = OffTopicChannelNameSerializer + + def get_object(self): + queryset = self.get_queryset() + name = self.kwargs[self.lookup_field] + return get_object_or_404(queryset, name=name) + + def get_queryset(self): + return OffTopicChannelName.objects.all() + + def create(self, request): + if 'name' in request.query_params: + create_data = {'name': request.query_params['name']} + serializer = OffTopicChannelNameSerializer(data=create_data) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(create_data, status=HTTP_201_CREATED) + + else: + raise ParseError(detail={ + 'name': ["This query parameter is required."] + }) + + def list(self, request): # noqa + if 'random_items' in request.query_params: + param = request.query_params['random_items'] + try: + random_count = int(param) + except ValueError: + raise ParseError(detail={'random_items': ["Must be a valid integer."]}) + + if random_count <= 0: + raise ParseError(detail={ + 'random_items': ["Must be a positive integer."] + }) + + queryset = self.get_queryset().order_by('?')[:random_count] + serialized = self.serializer_class(queryset, many=True) + return Response(serialized.data) + + queryset = self.get_queryset() + serialized = self.serializer_class(queryset, many=True) + return Response(serialized.data) diff --git a/pydis_site/apps/api/viewsets/bot/reminder.py b/pydis_site/apps/api/viewsets/bot/reminder.py new file mode 100644 index 00000000..d4ac8c76 --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/reminder.py @@ -0,0 +1,66 @@ +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework.filters import SearchFilter +from rest_framework.mixins import ( + CreateModelMixin, + DestroyModelMixin, + ListModelMixin +) +from rest_framework.viewsets import GenericViewSet + +from pydis_site.apps.api.models.bot.reminder import Reminder +from pydis_site.apps.api.serializers import ReminderSerializer + + +class ReminderViewSet(CreateModelMixin, ListModelMixin, DestroyModelMixin, GenericViewSet): + """ + View providing CRUD access to reminders. + + ## Routes + ### GET /bot/reminders + Returns all reminders in the database. + + #### Response format + >>> [ + ... { + ... 'active': True, + ... 'author': 1020103901030, + ... 'content': "Make dinner", + ... 'expiration': '5018-11-20T15:52:00Z', + ... 'id': 11 + ... }, + ... ... + ... ] + + #### Status codes + - 200: returned on success + + ### POST /bot/reminders + Create a new reminder. + + #### Request body + >>> { + ... 'author': int, + ... 'content': str, + ... 'expiration': str # ISO-formatted datetime + ... } + + #### Status codes + - 201: returned on success + - 400: if the body format is invalid + - 404: if no user with the given ID could be found + + ### DELETE /bot/reminders/ + Delete the reminder with the given `id`. + + #### Status codes + - 204: returned on success + - 404: if a reminder with the given `id` does not exist + + ## Authentication + Requires an API token. + """ + + serializer_class = ReminderSerializer + queryset = Reminder.objects.prefetch_related('author') + filter_backends = (DjangoFilterBackend, SearchFilter) + filter_fields = ('active', 'author__id') diff --git a/pydis_site/apps/api/viewsets/bot/role.py b/pydis_site/apps/api/viewsets/bot/role.py new file mode 100644 index 00000000..0131b374 --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/role.py @@ -0,0 +1,95 @@ +from rest_framework.viewsets import ModelViewSet + +from pydis_site.apps.api.models.bot.role import Role +from pydis_site.apps.api.serializers import RoleSerializer + + +class RoleViewSet(ModelViewSet): + """ + View providing CRUD access to the roles on our server, used + by the bot to keep a mirror of our server's roles on the site. + + ## Routes + ### GET /bot/roles + Returns all roles in the database. + + #### Response format + >>> [ + ... { + ... 'id': 267628507062992896, + ... 'name': "Admins", + ... 'colour': 1337, + ... 'permissions': 8 + ... } + ... ] + + #### Status codes + - 200: returned on success + + ### GET /bot/roles/ + Gets a single role by ID. + + #### Response format + >>> { + ... 'id': 267628507062992896, + ... 'name': "Admins", + ... 'colour': 1337, + ... 'permissions': 8 + ... } + + #### Status codes + - 200: returned on success + - 404: if a role with the given `snowflake` could not be found + + ### POST /bot/roles + Adds a single new role. + + #### Request body + >>> { + ... 'id': int, + ... 'name': str, + ... 'colour': int, + ... 'permissions': int, + ... } + + #### Status codes + - 201: returned on success + - 400: if the body format is invalid + + ### PUT /bot/roles/ + Update the role with the given `snowflake`. + All fields in the request body are required. + + #### Request body + >>> { + ... 'id': int, + ... 'name': str, + ... 'colour': int, + ... 'permissions': int + ... } + + #### Status codes + - 200: returned on success + - 400: if the request body was invalid + + ### PATCH /bot/roles/ + Update the role with the given `snowflake`. + All fields in the request body are required. + + >>> { + ... 'id': int, + ... 'name': str, + ... 'colour': int, + ... 'permissions': int + ... } + + ### DELETE /bot/roles/ + Deletes the role with the given `snowflake`. + + #### Status codes + - 204: returned on success + - 404: if a role with the given `snowflake` does not exist + """ + + queryset = Role.objects.all() + serializer_class = RoleSerializer diff --git a/pydis_site/apps/api/viewsets/bot/snake_fact.py b/pydis_site/apps/api/viewsets/bot/snake_fact.py new file mode 100644 index 00000000..0b2e8ede --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/snake_fact.py @@ -0,0 +1,30 @@ +from rest_framework.mixins import ListModelMixin +from rest_framework.viewsets import GenericViewSet + +from pydis_site.apps.api.models.bot.snake_fact import SnakeFact +from pydis_site.apps.api.serializers import SnakeFactSerializer + + +class SnakeFactViewSet(ListModelMixin, GenericViewSet): + """ + View providing snake facts created by the Pydis community in the first code jam. + + ## Routes + ### GET /bot/snake-facts + Returns snake facts from the database. + + #### Response format + >>> [ + ... {'fact': 'Snakes are dangerous'}, + ... {'fact': 'Except for Python, we all love it'} + ... ] + + #### Status codes + - 200: returned on success + + ## Authentication + Requires an API token. + """ + + serializer_class = SnakeFactSerializer + queryset = SnakeFact.objects.all() diff --git a/pydis_site/apps/api/viewsets/bot/snake_idiom.py b/pydis_site/apps/api/viewsets/bot/snake_idiom.py new file mode 100644 index 00000000..9f274d2f --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/snake_idiom.py @@ -0,0 +1,30 @@ +from rest_framework.mixins import ListModelMixin +from rest_framework.viewsets import GenericViewSet + +from pydis_site.apps.api.models.bot.snake_idiom import SnakeIdiom +from pydis_site.apps.api.serializers import SnakeIdiomSerializer + + +class SnakeIdiomViewSet(ListModelMixin, GenericViewSet): + """ + View providing snake idioms for the snake cog. + + ## Routes + ### GET /bot/snake-idioms + Returns snake idioms from the database. + + #### Response format + >>> [ + ... {'idiom': 'Sneky snek'}, + ... {'idiom': 'Snooky Snake'} + ... ] + + #### Status codes + - 200: returned on success + + ## Authentication + Requires an API token + """ + + serializer_class = SnakeIdiomSerializer + queryset = SnakeIdiom.objects.all() diff --git a/pydis_site/apps/api/viewsets/bot/snake_name.py b/pydis_site/apps/api/viewsets/bot/snake_name.py new file mode 100644 index 00000000..991706f5 --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/snake_name.py @@ -0,0 +1,61 @@ +from rest_framework.response import Response +from rest_framework.viewsets import ViewSet + +from pydis_site.apps.api.models.bot.snake_name import SnakeName +from pydis_site.apps.api.serializers import SnakeNameSerializer + + +class SnakeNameViewSet(ViewSet): + """ + View providing snake names for the bot's snake cog from our first code jam's winners. + + ## Routes + ### GET /bot/snake-names + By default, return a single random snake name along with its name and scientific name. + If the `get_all` query parameter is given, for example using... + $ curl api.pythondiscord.local:8000/bot/snake-names?get_all=yes + ... then the API will return all snake names and scientific names in the database. + + #### Response format + Without `get_all` query parameter: + >>> { + ... 'name': "Python", + ... 'scientific': "Langus greatus" + ... } + + If the database is empty for whatever reason, this will return an empty dictionary. + + With `get_all` query parameter: + >>> [ + ... {'name': "Python 3", 'scientific': "Langus greatus"}, + ... {'name': "Python 2", 'scientific': "Langus decentus"} + ... ] + + #### Status codes + - 200: returned on success + + ## Authentication + Requires a API token. + """ + + serializer_class = SnakeNameSerializer + + def get_queryset(self): + return SnakeName.objects.all() + + def list(self, request): # noqa + if request.query_params.get('get_all'): + queryset = self.get_queryset() + serialized = self.serializer_class(queryset, many=True) + return Response(serialized.data) + + single_snake = SnakeName.objects.order_by('?').first() + if single_snake is not None: + body = { + 'name': single_snake.name, + 'scientific': single_snake.scientific + } + + return Response(body) + + return Response({}) diff --git a/pydis_site/apps/api/viewsets/bot/special_snake.py b/pydis_site/apps/api/viewsets/bot/special_snake.py new file mode 100644 index 00000000..446c79a1 --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/special_snake.py @@ -0,0 +1,33 @@ +from rest_framework.mixins import ListModelMixin +from rest_framework.viewsets import GenericViewSet + +from pydis_site.apps.api.models.bot import SpecialSnake +from pydis_site.apps.api.serializers import SpecialSnakeSerializer + + +class SpecialSnakeViewSet(ListModelMixin, GenericViewSet): + """ + View providing special snake names for our bot's snake cog. + + ## Routes + ### GET /bot/special-snakes + Returns a list of special snake names. + + #### Response Format + >>> [ + ... { + ... 'name': 'Snakky sneakatus', + ... 'info': 'Scary snek', + ... 'image': 'https://discordapp.com/assets/53ef346458017da2062aca5c7955946b.svg' + ... } + ... ] + + #### Status codes + - 200: returned on success + + ## Authentication + Requires an API token. + """ + + serializer_class = SpecialSnakeSerializer + queryset = SpecialSnake.objects.all() diff --git a/pydis_site/apps/api/viewsets/bot/tag.py b/pydis_site/apps/api/viewsets/bot/tag.py new file mode 100644 index 00000000..7e9ba117 --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/tag.py @@ -0,0 +1,105 @@ +from rest_framework.viewsets import ModelViewSet + +from pydis_site.apps.api.models.bot.tag import Tag +from pydis_site.apps.api.serializers import TagSerializer + + +class TagViewSet(ModelViewSet): + """ + View providing CRUD operations on tags shown by our bot. + + ## Routes + ### GET /bot/tags + Returns all tags in the database. + + #### Response format + >>> [ + ... { + ... 'title': "resources", + ... 'embed': { + ... 'content': "Did you really think I'd put something useful here?" + ... } + ... } + ... ] + + #### Status codes + - 200: returned on success + + ### GET /bot/tags/ + Gets a single tag by its title. + + #### Response format + >>> { + ... 'title': "My awesome tag", + ... 'embed': { + ... 'content': "totally not filler words" + ... } + ... } + + #### Status codes + - 200: returned on success + - 404: if a tag with the given `title` could not be found + + ### POST /bot/tags + Adds a single tag to the database. + + #### Request body + >>> { + ... 'title': str, + ... 'embed': dict + ... } + + The embed structure is the same as the embed structure that the Discord API + expects. You can view the documentation for it here: + https://discordapp.com/developers/docs/resources/channel#embed-object + + #### Status codes + - 201: returned on success + - 400: if one of the given fields is invalid + + ### PUT /bot/tags/ + Update the tag with the given `title`. + + #### Request body + >>> { + ... 'title': str, + ... 'embed': dict + ... } + + The embed structure is the same as the embed structure that the Discord API + expects. You can view the documentation for it here: + https://discordapp.com/developers/docs/resources/channel#embed-object + + #### Status codes + - 200: returned on success + - 400: if the request body was invalid, see response body for details + - 404: if the tag with the given `title` could not be found + + ### PATCH /bot/tags/ + Update the tag with the given `title`. + + #### Request body + >>> { + ... 'title': str, + ... 'embed': dict + ... } + + The embed structure is the same as the embed structure that the Discord API + expects. You can view the documentation for it here: + https://discordapp.com/developers/docs/resources/channel#embed-object + + #### Status codes + - 200: returned on success + - 400: if the request body was invalid, see response body for details + - 404: if the tag with the given `title` could not be found + + ### DELETE /bot/tags/ + Deletes the tag with the given `title`. + + #### Status codes + - 204: returned on success + - 404: if a tag with the given `title` does not exist + """ + + serializer_class = TagSerializer + queryset = Tag.objects.all() diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py new file mode 100644 index 00000000..a407787e --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -0,0 +1,126 @@ +from rest_framework.viewsets import ModelViewSet +from rest_framework_bulk import BulkCreateModelMixin + +from pydis_site.apps.api.models.bot.user import User +from pydis_site.apps.api.serializers import UserSerializer + + +class UserViewSet(BulkCreateModelMixin, ModelViewSet): + """ + View providing CRUD operations on Discord users through the bot. + + ## Routes + ### GET /bot/users + Returns all users currently known. + + #### Response format + >>> [ + ... { + ... 'id': 409107086526644234, + ... 'avatar': "3ba3c1acce584c20b1e96fc04bbe80eb", + ... 'name': "Python", + ... 'discriminator': 4329, + ... 'roles': [ + ... 352427296948486144, + ... 270988689419665409, + ... 277546923144249364, + ... 458226699344019457 + ... ], + ... 'in_guild': True + ... } + ... ] + + #### Status codes + - 200: returned on success + + ### GET /bot/users/ + Gets a single user by ID. + + #### Response format + >>> { + ... 'id': 409107086526644234, + ... 'avatar': "3ba3c1acce584c20b1e96fc04bbe80eb", + ... 'name': "Python", + ... 'discriminator': 4329, + ... 'roles': [ + ... 352427296948486144, + ... 270988689419665409, + ... 277546923144249364, + ... 458226699344019457 + ... ], + ... 'in_guild': True + ... } + + #### Status codes + - 200: returned on success + - 404: if a user with the given `snowflake` could not be found + + ### POST /bot/users + Adds a single or multiple new users. + The roles attached to the user(s) must be roles known by the site. + + #### Request body + >>> { + ... 'id': int, + ... 'avatar': str, + ... 'name': str, + ... 'discriminator': int, + ... 'roles': List[int], + ... 'in_guild': bool + ... } + + Alternatively, request users can be POSTed as a list of above objects, + in which case multiple users will be created at once. + + #### Status codes + - 201: returned on success + - 400: if one of the given roles does not exist, or one of the given fields is invalid + + ### PUT /bot/users/ + Update the user with the given `snowflake`. + All fields in the request body are required. + + #### Request body + >>> { + ... 'id': int, + ... 'avatar': str, + ... 'name': str, + ... 'discriminator': int, + ... 'roles': List[int], + ... 'in_guild': bool + ... } + + #### Status codes + - 200: returned on success + - 400: if the request body was invalid, see response body for details + - 404: if the user with the given `snowflake` could not be found + + ### PATCH /bot/users/ + Update the user with the given `snowflake`. + All fields in the request body are optional. + + #### Request body + >>> { + ... 'id': int, + ... 'avatar': str, + ... 'name': str, + ... 'discriminator': int, + ... 'roles': List[int], + ... 'in_guild': bool + ... } + + #### Status codes + - 200: returned on success + - 400: if the request body was invalid, see response body for details + - 404: if the user with the given `snowflake` could not be found + + ### DELETE /bot/users/ + Deletes the user with the given `snowflake`. + + #### Status codes + - 204: returned on success + - 404: if a user with the given `snowflake` does not exist + """ + + serializer_class = UserSerializer + queryset = User.objects.prefetch_related('roles') diff --git a/pydis_site/apps/api/viewsets/log_entry.py b/pydis_site/apps/api/viewsets/log_entry.py new file mode 100644 index 00000000..4aa7dffa --- /dev/null +++ b/pydis_site/apps/api/viewsets/log_entry.py @@ -0,0 +1,37 @@ +from rest_framework.mixins import CreateModelMixin +from rest_framework.viewsets import GenericViewSet + +from pydis_site.apps.api.models.log_entry import LogEntry +from pydis_site.apps.api.serializers import LogEntrySerializer + + +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 -- cgit v1.2.3 From a5dee7db9a75bb50035ee3b3246cea6aa1cf5986 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Thu, 18 Apr 2019 19:48:42 +0200 Subject: Specify hadolint arguments on the command line. --- .hadolint.yaml | 4 ---- azure-pipelines.yml | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) delete mode 100644 .hadolint.yaml diff --git a/.hadolint.yaml b/.hadolint.yaml deleted file mode 100644 index c3c3449b..00000000 --- a/.hadolint.yaml +++ /dev/null @@ -1,4 +0,0 @@ -ignored: - - DL3008 # Ignore suggestion for pinned versions in `apt-get`... - - DL3013 # ... and `pip`. - - DL3018 # ... and `apk`. diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 35fb9f82..398b4797 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -19,7 +19,7 @@ jobs: - script: docker run -v $(pwd):/app:ro --rm ruby:alpine /bin/ash -c "gem install mdl && cd /app && mdl" displayName: run markdownlint - - script: docker run -i hadolint/hadolint hadolint --ignore DL3008 --ignore DL3018 --ignore DL3019 - < docker/app/Dockerfile + - script: docker run -i hadolint/hadolint hadolint --ignore DL3008 --ignore DL3013 --ignore DL3018 --ignore DL3019 - < docker/app/Dockerfile displayName: run hadolint - job: lint_python -- cgit v1.2.3 From 88b843be6dbe3e43f913c4fefa65e07e560474c7 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 19 Apr 2019 09:52:52 +1000 Subject: Remove multi-version, use lock and publish lint. --- azure-pipelines.yml | 229 ++++++++++++++++++++++------------------------------ 1 file changed, 96 insertions(+), 133 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 398b4797..dde9f574 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,136 +1,99 @@ # https://aka.ms/yaml jobs: - - job: lint_misc - displayName: Lint others - pool: - vmImage: ubuntu-16.04 - - steps: - - script: | - set -eux - - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - - sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" - sudo apt-get update - sudo apt-get install docker-ce - displayName: install docker - - - script: docker run -v $(pwd):/app:ro --rm ruby:alpine /bin/ash -c "gem install mdl && cd /app && mdl" - displayName: run markdownlint - - - script: docker run -i hadolint/hadolint hadolint --ignore DL3008 --ignore DL3013 --ignore DL3018 --ignore DL3019 - < docker/app/Dockerfile - displayName: run hadolint - - - job: lint_python - displayName: Lint Python - pool: - vmImage: ubuntu-16.04 - - strategy: - matrix: - Python 3.6: - python.version: 3.6 - Python 3.7: - python.version: 3.7 - - variables: - PIP_CACHE_DIR: .cache/pip - - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: $(python.version) - architecture: x64 - - - script: > - python -m pip install $(grep -E '^(coverage|flake8|mccabe|pep8-naming)' Pipfile | cut -d' ' -f1) - displayName: install lint requirements - - - script: flake8 - displayName: lint using flake8 - - - job: test - displayName: Test - dependsOn: - - lint_misc - - lint_python - pool: - vmImage: ubuntu-16.04 - strategy: - matrix: - Python 3.6 with PostgreSQL 10: - python.version: 3.6 - postgres.version: 10 - Python 3.6 with PostgreSQL 11: - python.version: 3.6 - postgres.version: 11 - Python 3.7 with PostgreSQL 10: - python.version: 3.7 - postgres.version: 10 - Python 3.7 with PostgreSQL 11: - python.version: 3.7 - postgres.version: 11 - - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: $(python.version) - architecture: x64 - - - script: | - curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - - sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' - sudo apt-get update -y - sudo apt-get install -y postgresql-$(postgres.version) - displayName: install postgresql - - - script: | - echo "$USER_CREATE_COMMAND;" > pgscript.sql - echo "CREATE DATABASE pysite OWNER pysite;" >> pgscript.sql - sudo su postgres -c "psql < pgscript.sql" - env: - USER_CREATE_COMMAND: CREATE USER pysite WITH PASSWORD 'pysite' CREATEDB - displayName: set up the database - - - script: python -m pip install pipenv && python -m pipenv install --dev --system - displayName: install requirements - - - script: | - python manage.py migrate - coverage run --branch manage.py test --testrunner xmlrunner.extra.djangotestrunner.XMLTestRunner --no-input - env: - CI: azure - DATABASE_URL: postgres://pysite:pysite@localhost/pysite - displayName: run tests - - - script: coverage report - displayName: show coverage results - - - task: PublishTestResults@2 - inputs: - testResultsFiles: "**/TEST-*.xml" - testRunTitle: 'Python $(python.version) with PostgreSQL $(postgres.version)' - - - job: push_image - displayName: Push Docker image - dependsOn: test - condition: and(succeeded(), eq(variables['build.sourceBranch'], 'refs/heads/django')) - pool: - vmImage: ubuntu-16.04 - - steps: - - task: Docker@1 - displayName: Login to Docker Hub - - inputs: - containerregistrytype: 'Container Registry' - dockerRegistryEndpoint: 'DockerHub' - command: 'login' - - - script: | - docker build -t pythondiscord/django:latest docker/app/Dockerfile - docker push pythondiscord/django:latest - displayName: Build and push the image - -# vim: sw=2 ts=2: +- job: lint + displayName: Lint + pool: + vmImage: ubuntu-16.04 + + variables: + PIP_CACHE_DIR: .cache/pip + + steps: + - task: UsePythonVersion@0 + displayName: Set Python Version + inputs: + versionSpec: '3.7.x' + addToPath: true + + - script: | + python3 -m pip install pipenv + pipenv install --dev --system + python3 -m pip install flake8-formatter-junit-xml + displayName: Install Requirements + + - script: python3 -m flake8 --format junit-xml --output-file test-lint.xml + displayName: Run Linter + + - task: PublishTestResults@2 + condition: succeededOrFailed() + inputs: + testResultsFiles: '**/test-*.xml' + testRunTitle: Site-Django Lint Results + +- job: test + displayName: Test + dependsOn: lint + pool: + vmImage: ubuntu-16.04 + + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.7.x' + architecture: x64 + + - script: | + curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - + sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' + sudo apt-get update -y + sudo apt-get install -y postgresql-11 + displayName: Install PostgreSQL + + - script: | + echo "$USER_CREATE_COMMAND;" > pgscript.sql + echo "CREATE DATABASE pysite OWNER pysite;" >> pgscript.sql + sudo su postgres -c "psql < pgscript.sql" + env: + USER_CREATE_COMMAND: CREATE USER pysite WITH PASSWORD 'pysite' CREATEDB + displayName: Setup Database + + - script: python3 -m pip install pipenv && pipenv install --dev --system + displayName: Install Requirements + + - script: | + python manage.py migrate + coverage run --branch manage.py test --testrunner xmlrunner.extra.djangotestrunner.XMLTestRunner --no-input + env: + CI: azure + DATABASE_URL: postgres://pysite:pysite@localhost/pysite + displayName: Run Test + + - script: coverage report + displayName: Show Coverage Results + + - task: PublishTestResults@2 + inputs: + testResultsFiles: "**/TEST-*.xml" + testRunTitle: Site-Django Test Results + +- job: build + displayName: Build + dependsOn: test + condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) + pool: + vmImage: ubuntu-16.04 + + steps: + - task: Docker@1 + displayName: Login to Docker Hub + + inputs: + containerregistrytype: 'Container Registry' + dockerRegistryEndpoint: 'DockerHub' + command: 'login' + + - script: | + docker build -t pythondiscord/django:latest docker/app/Dockerfile + docker push pythondiscord/django:latest + displayName: Build & Push Docker -- cgit v1.2.3 From 64cfbcb1ee8b11cb8f80cfcc672335ab7e26d174 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 19 Apr 2019 10:10:07 +1000 Subject: Fix docker reference. --- azure-pipelines.yml | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index dde9f574..0a768663 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -2,7 +2,7 @@ jobs: - job: lint - displayName: Lint + displayName: 'Lint' pool: vmImage: ubuntu-16.04 @@ -11,28 +11,25 @@ jobs: steps: - task: UsePythonVersion@0 - displayName: Set Python Version + displayName: 'Set Python Version' inputs: versionSpec: '3.7.x' addToPath: true - - script: | - python3 -m pip install pipenv - pipenv install --dev --system - python3 -m pip install flake8-formatter-junit-xml - displayName: Install Requirements + - script: python3 -m pip install pipenv && pipenv install --dev --system && python3 -m pip install flake8-formatter-junit-xml + displayName: 'Install Requirements' - script: python3 -m flake8 --format junit-xml --output-file test-lint.xml - displayName: Run Linter + displayName: 'Run Linter' - task: PublishTestResults@2 condition: succeededOrFailed() inputs: testResultsFiles: '**/test-*.xml' - testRunTitle: Site-Django Lint Results + testRunTitle: 'Site-Django Lint Results' - job: test - displayName: Test + displayName: 'Test' dependsOn: lint pool: vmImage: ubuntu-16.04 @@ -48,7 +45,7 @@ jobs: sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' sudo apt-get update -y sudo apt-get install -y postgresql-11 - displayName: Install PostgreSQL + displayName: 'Install PostgreSQL' - script: | echo "$USER_CREATE_COMMAND;" > pgscript.sql @@ -56,10 +53,10 @@ jobs: sudo su postgres -c "psql < pgscript.sql" env: USER_CREATE_COMMAND: CREATE USER pysite WITH PASSWORD 'pysite' CREATEDB - displayName: Setup Database + displayName: 'Setup Database' - script: python3 -m pip install pipenv && pipenv install --dev --system - displayName: Install Requirements + displayName: 'Install Requirements' - script: | python manage.py migrate @@ -67,15 +64,15 @@ jobs: env: CI: azure DATABASE_URL: postgres://pysite:pysite@localhost/pysite - displayName: Run Test + displayName: 'Run Test' - script: coverage report - displayName: Show Coverage Results + displayName: 'Show Coverage Results' - task: PublishTestResults@2 inputs: testResultsFiles: "**/TEST-*.xml" - testRunTitle: Site-Django Test Results + testRunTitle: 'Site-Django Test Results' - job: build displayName: Build @@ -94,6 +91,6 @@ jobs: command: 'login' - script: | - docker build -t pythondiscord/django:latest docker/app/Dockerfile + docker build -t pythondiscord/django:latest -f docker/Dockerfile . docker push pythondiscord/django:latest - displayName: Build & Push Docker + displayName: 'Build & Push Docker' -- cgit v1.2.3 From 8206bff7d81dda62bf75ec6bf1a5be8e00e1cc37 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 19 Apr 2019 10:20:39 +1000 Subject: Fix Dockerfile path. --- azure-pipelines.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 0a768663..f0a55448 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,7 +1,7 @@ # https://aka.ms/yaml jobs: -- job: lint +- job: python_lint displayName: 'Lint' pool: vmImage: ubuntu-16.04 @@ -17,7 +17,7 @@ jobs: addToPath: true - script: python3 -m pip install pipenv && pipenv install --dev --system && python3 -m pip install flake8-formatter-junit-xml - displayName: 'Install Requirements' + displayName: 'Install Project Environment' - script: python3 -m flake8 --format junit-xml --output-file test-lint.xml displayName: 'Run Linter' @@ -28,9 +28,9 @@ jobs: testResultsFiles: '**/test-*.xml' testRunTitle: 'Site-Django Lint Results' -- job: test +- job: coverage_test displayName: 'Test' - dependsOn: lint + dependsOn: python_lint pool: vmImage: ubuntu-16.04 @@ -56,7 +56,7 @@ jobs: displayName: 'Setup Database' - script: python3 -m pip install pipenv && pipenv install --dev --system - displayName: 'Install Requirements' + displayName: 'Install Project Environment' - script: | python manage.py migrate @@ -74,9 +74,9 @@ jobs: testResultsFiles: "**/TEST-*.xml" testRunTitle: 'Site-Django Test Results' -- job: build - displayName: Build - dependsOn: test +- job: docker + displayName: 'Build & Push' + dependsOn: coverage_test condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) pool: vmImage: ubuntu-16.04 @@ -91,6 +91,6 @@ jobs: command: 'login' - script: | - docker build -t pythondiscord/django:latest -f docker/Dockerfile . + docker build -t pythondiscord/django:latest -f docker/app/Dockerfile . docker push pythondiscord/django:latest - displayName: 'Build & Push Docker' + displayName: 'Build & Push Docker Image' -- cgit v1.2.3 From 473f662567b3f474cc97b715e260d87cf877fd07 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 19 Apr 2019 11:13:16 +1000 Subject: Get verbose output for linter. --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f0a55448..0723db57 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -19,7 +19,7 @@ jobs: - script: python3 -m pip install pipenv && pipenv install --dev --system && python3 -m pip install flake8-formatter-junit-xml displayName: 'Install Project Environment' - - script: python3 -m flake8 --format junit-xml --output-file test-lint.xml + - script: python3 -m flake8 -vv --format junit-xml --output-file test-lint.xml displayName: 'Run Linter' - task: PublishTestResults@2 -- cgit v1.2.3 From 5ec1093d62de69beb7a0002c49ef247708bccd1e Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 19 Apr 2019 11:17:40 +1000 Subject: Output verbose linter to console. --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 0723db57..9abc84d4 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -19,7 +19,7 @@ jobs: - script: python3 -m pip install pipenv && pipenv install --dev --system && python3 -m pip install flake8-formatter-junit-xml displayName: 'Install Project Environment' - - script: python3 -m flake8 -vv --format junit-xml --output-file test-lint.xml + - script: python3 -m flake8 -vv && python3 -m flake8 --format junit-xml --output-file test-lint.xml displayName: 'Run Linter' - task: PublishTestResults@2 -- cgit v1.2.3 From c4659b2ffc6b3cb2712a3d8be85b78b83363b547 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 19 Apr 2019 11:26:34 +1000 Subject: Revert linter to non-verbose. --- azure-pipelines.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 9abc84d4..9ca2b812 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -2,7 +2,7 @@ jobs: - job: python_lint - displayName: 'Lint' + displayName: 'Lint Job' pool: vmImage: ubuntu-16.04 @@ -19,7 +19,7 @@ jobs: - script: python3 -m pip install pipenv && pipenv install --dev --system && python3 -m pip install flake8-formatter-junit-xml displayName: 'Install Project Environment' - - script: python3 -m flake8 -vv && python3 -m flake8 --format junit-xml --output-file test-lint.xml + - script: python3 -m flake8 --format junit-xml --output-file test-lint.xml displayName: 'Run Linter' - task: PublishTestResults@2 @@ -29,7 +29,7 @@ jobs: testRunTitle: 'Site-Django Lint Results' - job: coverage_test - displayName: 'Test' + displayName: 'Test Job' dependsOn: python_lint pool: vmImage: ubuntu-16.04 @@ -75,7 +75,7 @@ jobs: testRunTitle: 'Site-Django Test Results' - job: docker - displayName: 'Build & Push' + displayName: 'Build & Push Job' dependsOn: coverage_test condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) pool: -- cgit v1.2.3