diff options
author | 2018-11-25 11:16:57 +0100 | |
---|---|---|
committer | 2018-11-25 11:16:57 +0100 | |
commit | 87a48cad5197234a6ccff616fec17a027b2adcb8 (patch) | |
tree | 0ea7bebddf9ce216e94602ddeca300184729fb00 | |
parent | Use proper attribute name. (diff) | |
parent | Set up image pushing and building on Azure. (#152) (diff) |
Merge branch 'django' into django+add-logs-api.
-rw-r--r-- | api/admin.py | 6 | ||||
-rw-r--r-- | api/migrations/0018_messagedeletioncontext.py | 2 | ||||
-rw-r--r-- | api/migrations/0018_user_rename.py | 17 | ||||
-rw-r--r-- | api/migrations/0019_deletedmessage.py | 2 | ||||
-rw-r--r-- | api/migrations/0019_user_in_guild.py | 18 | ||||
-rw-r--r-- | api/migrations/0020_add_snake_field_validators.py | 24 | ||||
-rw-r--r-- | api/migrations/0021_merge_20181125_1015.py | 14 | ||||
-rw-r--r-- | api/models.py | 18 | ||||
-rw-r--r-- | api/serializers.py | 18 | ||||
-rw-r--r-- | api/tests/test_models.py | 22 | ||||
-rw-r--r-- | api/tests/test_users.py (renamed from api/tests/test_members.py) | 30 | ||||
-rw-r--r-- | api/urls.py | 12 | ||||
-rw-r--r-- | api/viewsets.py | 81 | ||||
-rw-r--r-- | azure-pipelines.yml | 23 | ||||
-rw-r--r-- | pysite/settings.py | 19 |
15 files changed, 216 insertions, 90 deletions
diff --git a/api/admin.py b/api/admin.py index af2cfbeb..bcd41a7e 100644 --- a/api/admin.py +++ b/api/admin.py @@ -2,17 +2,16 @@ from django.contrib import admin from .models import ( DeletedMessage, DocumentationLink, - Member, MessageDeletionContext, + MessageDeletionContext, OffTopicChannelName, Role, SnakeFact, SnakeIdiom, SnakeName, SpecialSnake, - Tag + Tag, User ) admin.site.register(DeletedMessage) admin.site.register(DocumentationLink) -admin.site.register(Member) admin.site.register(MessageDeletionContext) admin.site.register(OffTopicChannelName) admin.site.register(Role) @@ -21,3 +20,4 @@ admin.site.register(SnakeIdiom) admin.site.register(SnakeName) admin.site.register(SpecialSnake) admin.site.register(Tag) +admin.site.register(User) diff --git a/api/migrations/0018_messagedeletioncontext.py b/api/migrations/0018_messagedeletioncontext.py index 39a4fb87..88cbab28 100644 --- a/api/migrations/0018_messagedeletioncontext.py +++ b/api/migrations/0018_messagedeletioncontext.py @@ -17,7 +17,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('creation', models.DateTimeField(help_text='When this deletion took place.')), - ('actor', models.ForeignKey(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, on_delete=django.db.models.deletion.CASCADE, to='api.Member')), + ('actor', models.ForeignKey(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, on_delete=django.db.models.deletion.CASCADE, to='api.User')), ], bases=(api.models.ModelReprMixin, models.Model), ), diff --git a/api/migrations/0018_user_rename.py b/api/migrations/0018_user_rename.py new file mode 100644 index 00000000..f88eb5bc --- /dev/null +++ b/api/migrations/0018_user_rename.py @@ -0,0 +1,17 @@ +# Generated by Django 2.1.3 on 2018-11-19 20:09 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0017_auto_20181029_1921'), + ] + + operations = [ + migrations.RenameModel( + old_name='Member', + new_name='User', + ), + ] diff --git a/api/migrations/0019_deletedmessage.py b/api/migrations/0019_deletedmessage.py index b119c3ef..fbd94949 100644 --- a/api/migrations/0019_deletedmessage.py +++ b/api/migrations/0019_deletedmessage.py @@ -23,7 +23,7 @@ class Migration(migrations.Migration): ('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=[api.validators.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.Member')), + ('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')), ], options={ diff --git a/api/migrations/0019_user_in_guild.py b/api/migrations/0019_user_in_guild.py new file mode 100644 index 00000000..fda008c4 --- /dev/null +++ b/api/migrations/0019_user_in_guild.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.3 on 2018-11-19 20:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0018_user_rename'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='in_guild', + field=models.BooleanField(default=True, help_text='Whether this user is in our server.'), + ), + ] diff --git a/api/migrations/0020_add_snake_field_validators.py b/api/migrations/0020_add_snake_field_validators.py new file mode 100644 index 00000000..3b625f9b --- /dev/null +++ b/api/migrations/0020_add_snake_field_validators.py @@ -0,0 +1,24 @@ +# Generated by Django 2.1.2 on 2018-11-24 17:11 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0019_user_in_guild'), + ] + + operations = [ + migrations.AlterField( + model_name='snakename', + name='name', + field=models.CharField(help_text="The regular name for this snake, e.g. 'Python'.", max_length=100, primary_key=True, serialize=False, validators=[django.core.validators.RegexValidator(regex='^([^0-9])+$')]), + ), + migrations.AlterField( + model_name='snakename', + name='scientific', + field=models.CharField(help_text="The scientific name for this snake, e.g. 'Python bivittatus'.", max_length=150, validators=[django.core.validators.RegexValidator(regex='^([^0-9])+$')]), + ), + ] diff --git a/api/migrations/0021_merge_20181125_1015.py b/api/migrations/0021_merge_20181125_1015.py new file mode 100644 index 00000000..d8eaa510 --- /dev/null +++ b/api/migrations/0021_merge_20181125_1015.py @@ -0,0 +1,14 @@ +# Generated by Django 2.1.1 on 2018-11-25 10:15 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0020_add_snake_field_validators'), + ('api', '0019_deletedmessage'), + ] + + operations = [ + ] diff --git a/api/models.py b/api/models.py index ded9ebeb..68833328 100644 --- a/api/models.py +++ b/api/models.py @@ -92,11 +92,13 @@ class SnakeName(ModelReprMixin, models.Model): name = models.CharField( primary_key=True, max_length=100, - help_text="The regular name for this snake, e.g. 'Python'." + 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'." + help_text="The scientific name for this snake, e.g. 'Python bivittatus'.", + validators=[RegexValidator(regex=r'^([^0-9])+$')] ) def __str__(self): @@ -167,8 +169,8 @@ class Role(ModelReprMixin, models.Model): return self.name -class Member(ModelReprMixin, models.Model): - """A member of our Discord server.""" +class User(ModelReprMixin, models.Model): + """A Discord user.""" id = models.BigIntegerField( # noqa primary_key=True, @@ -205,6 +207,10 @@ class Member(ModelReprMixin, models.Model): 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}" @@ -222,7 +228,7 @@ class Message(ModelReprMixin, models.Model): ) ) author = models.ForeignKey( - Member, + User, on_delete=models.CASCADE, help_text="The author of this message." ) @@ -255,7 +261,7 @@ class Message(ModelReprMixin, models.Model): class MessageDeletionContext(ModelReprMixin, models.Model): actor = models.ForeignKey( - Member, + User, on_delete=models.CASCADE, help_text=( "The original actor causing this deletion. Could be the author " diff --git a/api/serializers.py b/api/serializers.py index e39cd4a3..8091ac63 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -3,11 +3,11 @@ from rest_framework_bulk import BulkSerializerMixin from .models import ( DeletedMessage, DocumentationLink, - Member, MessageDeletionContext, - OffTopicChannelName, Role, - SnakeFact, SnakeIdiom, - SnakeName, SpecialSnake, - Tag + MessageDeletionContext, OffTopicChannelName, + Role, SnakeFact, + SnakeIdiom, SnakeName, + SpecialSnake, Tag, + User ) @@ -74,10 +74,10 @@ class TagSerializer(ModelSerializer): fields = ('title', 'embed') -class MemberSerializer(BulkSerializerMixin, ModelSerializer): - roles = PrimaryKeyRelatedField(many=True, queryset=Role.objects.all()) +class UserSerializer(BulkSerializerMixin, ModelSerializer): + roles = PrimaryKeyRelatedField(many=True, queryset=Role.objects.all(), required=False) class Meta: - model = Member - fields = ('id', 'avatar_hash', 'name', 'discriminator', 'roles') + model = User + fields = ('id', 'avatar_hash', 'name', 'discriminator', 'roles', 'in_guild') depth = 1 diff --git a/api/tests/test_models.py b/api/tests/test_models.py index 8d41c23e..968f003e 100644 --- a/api/tests/test_models.py +++ b/api/tests/test_models.py @@ -4,12 +4,12 @@ from django.test import SimpleTestCase from ..models import ( DeletedMessage, DocumentationLink, - Member, Message, - MessageDeletionContext, ModelReprMixin, - OffTopicChannelName, Role, - SnakeFact, SnakeIdiom, - SnakeName, SpecialSnake, - Tag + Message, MessageDeletionContext, + ModelReprMixin, OffTopicChannelName, + Role, SnakeFact, + SnakeIdiom, SnakeName, + SpecialSnake, Tag, + User ) @@ -32,14 +32,14 @@ class StringDunderMethodTests(SimpleTestCase): self.objects = ( DeletedMessage( id=45, - author=Member( + author=User( id=444, name='bill', discriminator=5, avatar_hash=None ), channel_id=666, content="wooey", deletion_context=MessageDeletionContext( - actor=Member( + actor=User( id=5555, name='shawn', discriminator=555, avatar_hash=None ), @@ -64,7 +64,7 @@ class StringDunderMethodTests(SimpleTestCase): ), Message( id=45, - author=Member( + author=User( id=444, name='bill', discriminator=5, avatar_hash=None ), @@ -73,13 +73,13 @@ class StringDunderMethodTests(SimpleTestCase): embeds=[] ), MessageDeletionContext( - actor=Member( + actor=User( id=5555, name='shawn', discriminator=555, avatar_hash=None ), creation=datetime.utcnow() ), - Member( + User( id=5, name='bob', discriminator=1, avatar_hash=None ), diff --git a/api/tests/test_members.py b/api/tests/test_users.py index 47466b62..8dadcbdb 100644 --- a/api/tests/test_members.py +++ b/api/tests/test_users.py @@ -1,7 +1,7 @@ from django_hosts.resolvers import reverse from .base import APISubdomainTestCase -from ..models import Member, Role +from ..models import Role, User class UnauthedDocumentationLinkAPITests(APISubdomainTestCase): @@ -10,25 +10,25 @@ class UnauthedDocumentationLinkAPITests(APISubdomainTestCase): self.client.force_authenticate(user=None) def test_detail_lookup_returns_401(self): - url = reverse('bot:member-detail', args=('whatever',), host='api') + url = reverse('bot:user-detail', args=('whatever',), host='api') response = self.client.get(url) self.assertEqual(response.status_code, 401) def test_list_returns_401(self): - url = reverse('bot:member-list', host='api') + url = reverse('bot:user-list', host='api') response = self.client.get(url) self.assertEqual(response.status_code, 401) def test_create_returns_401(self): - url = reverse('bot:member-list', host='api') + url = reverse('bot:user-list', host='api') response = self.client.post(url, data={'hi': 'there'}) self.assertEqual(response.status_code, 401) def test_delete_returns_401(self): - url = reverse('bot:member-detail', args=('whatever',), host='api') + url = reverse('bot:user-detail', args=('whatever',), host='api') response = self.client.delete(url) self.assertEqual(response.status_code, 401) @@ -45,7 +45,7 @@ class CreationTests(APISubdomainTestCase): ) def test_accepts_valid_data(self): - url = reverse('bot:member-list', host='api') + url = reverse('bot:user-list', host='api') data = { 'id': 42, 'avatar_hash': "validavatarhashiswear", @@ -53,20 +53,22 @@ class CreationTests(APISubdomainTestCase): 'discriminator': 42, 'roles': [ self.role.id - ] + ], + 'in_guild': True } response = self.client.post(url, data=data) self.assertEqual(response.status_code, 201) self.assertEqual(response.json(), data) - user = Member.objects.get(id=42) + user = User.objects.get(id=42) self.assertEqual(user.avatar_hash, data['avatar_hash']) self.assertEqual(user.name, data['name']) self.assertEqual(user.discriminator, data['discriminator']) + self.assertEqual(user.in_guild, data['in_guild']) def test_supports_multi_creation(self): - url = reverse('bot:member-list', host='api') + url = reverse('bot:user-list', host='api') data = [ { 'id': 5, @@ -75,14 +77,16 @@ class CreationTests(APISubdomainTestCase): 'discriminator': 42, 'roles': [ self.role.id - ] + ], + 'in_guild': True }, { 'id': 8, 'avatar_hash': "maybenot", 'name': "another test man", 'discriminator': 555, - 'roles': [] + 'roles': [], + 'in_guild': False } ] @@ -91,7 +95,7 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response.json(), data) def test_returns_400_for_unknown_role_id(self): - url = reverse('bot:member-list', host='api') + url = reverse('bot:user-list', host='api') data = { 'id': 5, 'avatar_hash': "hahayes", @@ -106,7 +110,7 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response.status_code, 400) def test_returns_400_for_bad_data(self): - url = reverse('bot:member-list', host='api') + url = reverse('bot:user-list', host='api') data = { 'id': True, 'avatar_hash': 1902831, diff --git a/api/urls.py b/api/urls.py index 2bca5689..dca208d8 100644 --- a/api/urls.py +++ b/api/urls.py @@ -4,10 +4,10 @@ from rest_framework.routers import DefaultRouter from .views import HealthcheckView from .viewsets import ( DeletedMessageViewSet, DocumentationLinkViewSet, - MemberViewSet, OffTopicChannelNameViewSet, - RoleViewSet, SnakeFactViewSet, - SnakeIdiomViewSet, SnakeNameViewSet, - SpecialSnakeViewSet, TagViewSet + OffTopicChannelNameViewSet, RoleViewSet, + SnakeFactViewSet, SnakeIdiomViewSet, + SnakeNameViewSet, SpecialSnakeViewSet, + TagViewSet, UserViewSet ) @@ -27,8 +27,8 @@ bot_router.register( base_name='offtopicchannelname' ) bot_router.register( - 'members', - MemberViewSet + 'users', + UserViewSet ) bot_router.register( 'roles', diff --git a/api/viewsets.py b/api/viewsets.py index 8dca5b2a..e406e8c3 100644 --- a/api/viewsets.py +++ b/api/viewsets.py @@ -10,18 +10,18 @@ from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet from rest_framework_bulk import BulkCreateModelMixin from .models import ( - DocumentationLink, Member, - MessageDeletionContext, OffTopicChannelName, - Role, SnakeFact, - SnakeIdiom, SnakeName, - SpecialSnake, Tag + DocumentationLink, MessageDeletionContext, + OffTopicChannelName, Role, + SnakeFact, SnakeIdiom, + SnakeName, SpecialSnake, + Tag, User ) from .serializers import ( - DocumentationLinkSerializer, MemberSerializer, - MessageDeletionContextSerializer, OffTopicChannelNameSerializer, - RoleSerializer, SnakeFactSerializer, - SnakeIdiomSerializer, SnakeNameSerializer, - SpecialSnakeSerializer, TagSerializer + DocumentationLinkSerializer, MessageDeletionContextSerializer, + OffTopicChannelNameSerializer, RoleSerializer, + SnakeFactSerializer, SnakeIdiomSerializer, + SnakeNameSerializer, SpecialSnakeSerializer, + TagSerializer, UserSerializer ) @@ -539,13 +539,13 @@ class TagViewSet(ModelViewSet): queryset = Tag.objects.all() -class MemberViewSet(BulkCreateModelMixin, ModelViewSet): +class UserViewSet(BulkCreateModelMixin, ModelViewSet): """ - View providing CRUD operations on our Discord server's members through the bot. + View providing CRUD operations on Discord users through the bot. ## Routes - ### GET /bot/members - Returns all members currently known. + ### GET /bot/users + Returns all users currently known. #### Response format >>> [ @@ -559,15 +559,16 @@ class MemberViewSet(BulkCreateModelMixin, ModelViewSet): ... 270988689419665409, ... 277546923144249364, ... 458226699344019457 - ... ] + ... ], + ... 'in_guild': True ... } ... ] #### Status codes - 200: returned on success - ### GET /bot/members/<snowflake:int> - Gets a single member by ID. + ### GET /bot/users/<snowflake:int> + Gets a single user by ID. #### Response format >>> { @@ -580,16 +581,17 @@ class MemberViewSet(BulkCreateModelMixin, ModelViewSet): ... 270988689419665409, ... 277546923144249364, ... 458226699344019457 - ... ] + ... ], + ... 'in_guild': True ... } #### Status codes - 200: returned on success - - 404: if a member with the given `snowflake` could not be found + - 404: if a user with the given `snowflake` could not be found - ### POST /bot/members - Adds a single or multiple new members. - The roles attached to the member(s) must be roles known by the site. + ### 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 >>> { @@ -597,18 +599,19 @@ class MemberViewSet(BulkCreateModelMixin, ModelViewSet): ... 'avatar': str, ... 'name': str, ... 'discriminator': int, - ... 'roles': List[int] + ... 'roles': List[int], + ... 'in_guild': bool ... } - Alternatively, request members can be POSTed as a list of above objects, - in which case multiple members will be created at once. + 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/members/<snowflake:int> - Update the member with the given `snowflake`. + ### PUT /bot/users/<snowflake:int> + Update the user with the given `snowflake`. All fields in the request body are required. #### Request body @@ -617,16 +620,17 @@ class MemberViewSet(BulkCreateModelMixin, ModelViewSet): ... 'avatar': str, ... 'name': str, ... 'discriminator': int, - ... 'roles': List[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 member with the given `snowflake` could not be found + - 404: if the user with the given `snowflake` could not be found - ### PATCH /bot/members/<snowflake:int> - Update the member with the given `snowflake`. + ### PATCH /bot/users/<snowflake:int> + Update the user with the given `snowflake`. All fields in the request body are optional. #### Request body @@ -635,21 +639,22 @@ class MemberViewSet(BulkCreateModelMixin, ModelViewSet): ... 'avatar': str, ... 'name': str, ... 'discriminator': int, - ... 'roles': List[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 member with the given `snowflake` could not be found + - 404: if the user with the given `snowflake` could not be found - ### DELETE /bot/members/<snowflake:int> - Deletes the member with the given `snowflake`. + ### DELETE /bot/users/<snowflake:int> + Deletes the user with the given `snowflake`. #### Status codes - 204: returned on success - - 404: if a member with the given `snowflake` does not exist + - 404: if a user with the given `snowflake` does not exist """ - serializer_class = MemberSerializer - queryset = Member.objects.all() + serializer_class = UserSerializer + queryset = User.objects.all() diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 90232430..1c6a746a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -55,6 +55,9 @@ jobs: - job: test displayName: Test + dependsOn: + - lint_misc + - lint_python pool: vmImage: ubuntu-16.04 strategy: @@ -112,5 +115,25 @@ jobs: 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 push pythondiscord/django:latest + displayName: Build and push the image # vim: sw=2 ts=2: diff --git a/pysite/settings.py b/pysite/settings.py index 5badde5c..c3373250 100644 --- a/pysite/settings.py +++ b/pysite/settings.py @@ -11,7 +11,7 @@ https://docs.djangoproject.com/en/2.1/ref/settings/ """ import os -# import sys +import sys import environ @@ -207,7 +207,22 @@ LOGGING = { 'django': { 'handlers': ['console'], 'propagate': True, - 'level': 'INFO' if DEBUG else env('LOG_LEVEL', default='WARN') + 'level': env( + 'LOG_LEVEL', + default=( + # If there is no explicit `LOG_LEVEL` set, + # use `DEBUG` if we're running in debug mode but not + # testing. Use `ERROR` if we're running tests, else + # default to using `WARN`. + 'DEBUG' + if DEBUG and 'test' not in sys.argv + else ( + 'ERROR' + if 'test' in sys.argv + else 'WARN' + ) + ) + ) } } } |