From ab8b798547e82ca79882ba28b1920077c803425f Mon Sep 17 00:00:00 2001 From: Gareth Coles Date: Fri, 5 Apr 2019 18:24:32 +0100 Subject: pysite -> pydis_site --- pydis_site/apps/api/viewsets.py | 890 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 890 insertions(+) create mode 100644 pydis_site/apps/api/viewsets.py (limited to 'pydis_site/apps/api/viewsets.py') diff --git a/pydis_site/apps/api/viewsets.py b/pydis_site/apps/api/viewsets.py new file mode 100644 index 00000000..0471f79d --- /dev/null +++ b/pydis_site/apps/api/viewsets.py @@ -0,0 +1,890 @@ +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, 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 +) + + +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 pydis_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 pydis_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 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 pydis_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 pydis_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) -- cgit v1.2.3 From d576432e6145df464273f0f22d841cc9f5804b07 Mon Sep 17 00:00:00 2001 From: Gareth Coles Date: Fri, 5 Apr 2019 20:59:22 +0100 Subject: Address review by @jchristgit --- Dockerfile | 1 - Pipfile | 42 +++++++++++----------- Pipfile.lock | 2 +- azure-pipelines.yml | 15 +++----- docker-compose.yml | 8 ++--- docker/app/Dockerfile | 42 ++++++++++++++++++++++ docker/app/alpine/3.6/Dockerfile | 33 ----------------- docker/app/alpine/3.7/Dockerfile | 32 ----------------- docker/app/stretch/3.6/Dockerfile | 39 -------------------- docker/app/stretch/3.7/Dockerfile | 39 -------------------- docker/pysite.dockerapp | 2 +- .../api/migrations/0008_tag_embed_validator.py | 5 +-- .../apps/api/migrations/0019_deletedmessage.py | 5 +-- pydis_site/apps/api/views.py | 2 +- pydis_site/apps/api/viewsets.py | 8 ++--- 15 files changed, 81 insertions(+), 194 deletions(-) delete mode 120000 Dockerfile create mode 100644 docker/app/Dockerfile delete mode 100644 docker/app/alpine/3.6/Dockerfile delete mode 100644 docker/app/alpine/3.7/Dockerfile delete mode 100644 docker/app/stretch/3.6/Dockerfile delete mode 100644 docker/app/stretch/3.7/Dockerfile (limited to 'pydis_site/apps/api/viewsets.py') diff --git a/Dockerfile b/Dockerfile deleted file mode 120000 index 9b50dcb7..00000000 --- a/Dockerfile +++ /dev/null @@ -1 +0,0 @@ -docker/app/alpine/3.7/Dockerfile \ No newline at end of file diff --git a/Pipfile b/Pipfile index 7a12f79d..932746d2 100644 --- a/Pipfile +++ b/Pipfile @@ -4,29 +4,29 @@ url = "https://pypi.org/simple" verify_ssl = true [dev-packages] -flake8 = "*" -flake8-bandit = "*" -flake8-bugbear = "*" -flake8-import-order = "*" -flake8-string-format = "*" -flake8-tidy-imports = "*" -mccabe = "*" -pep8-naming = "*" -coverage = "*" -unittest-xml-reporting = "*" +flake8 = "~=3.7.7" +flake8-bandit = "~=2.1.0" +flake8-bugbear = "~=19.3.0" +flake8-import-order = "~=0.18.1" +flake8-string-format = "~=0.2.3" +flake8-tidy-imports = "~=2.0.0" +mccabe = "~=0.6.1" +pep8-naming = "~=0.8.2" +coverage = "~=4.5.3" +unittest-xml-reporting = "~=2.5.1" [packages] -django = ">=2.2" -django-crispy-forms = "*" -django-environ = "*" -django-filter = "*" -django-hosts = "*" -djangorestframework = "*" -djangorestframework-bulk = "*" -uwsgi = "*" -psycopg2-binary = "*" -django-simple-bulma = "*" -django-crispy-bulma = "*" +django = "~=2.2" +django-crispy-forms = "~=1.7.2" +django-environ = "~=0.4.5" +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" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 9422d7b8..c574a6f1 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "8b0d5f0bcf74d3de74397538e673a96b56935085ed611f8d160213ca3af90109" + "sha256": "e7a956892e9d18b6ac8ab13fe8e139d29196e236fbb194f4cb8b37308ea91c6e" }, "pipfile-spec": 6, "requires": { diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 1e6d67f0..78bbffae 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -17,12 +17,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: | - echo 'set -ex' > script.sh - echo 'for dockerfile in docker/**/**/**/Dockerfile; do' >> script.sh - echo ' docker run -i hadolint/hadolint hadolint --ignore DL3008 --ignore DL3018 --ignore DL3019 - < $dockerfile' >> script.sh - echo 'done' >> script.sh - sh script.sh + - script: docker run -i hadolint/hadolint hadolint --ignore DL3008 --ignore DL3018 --ignore DL3019 - < docker/app/Dockerfile displayName: run hadolint - job: lint_python @@ -47,7 +42,7 @@ jobs: architecture: x64 - script: > - python -m pip install $(grep -E '^(coverage|flake8|mccabe|pep8-naming)' pyproject.toml | cut -d' ' -f1) + python -m pip install $(grep -E '^(coverage|flake8|mccabe|pep8-naming)' Pipfile | cut -d' ' -f1) displayName: install lint requirements - script: flake8 @@ -93,7 +88,7 @@ jobs: echo "CREATE DATABASE pysite OWNER pysite;" >> pgscript.sql sudo su postgres -c "psql < pgscript.sql" env: - USER_CREATE_COMMAND: CREATE USER pydis_site WITH PASSWORD 'pydis_site' CREATEDB + 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 @@ -104,7 +99,7 @@ jobs: coverage run --branch manage.py test --testrunner xmlrunner.extra.djangotestrunner.XMLTestRunner --no-input env: CI: azure - DATABASE_URL: postgres://pydis_site:pydis_site@localhost/pydis_site + DATABASE_URL: postgres://pysite:pysite@localhost/pysite displayName: run tests - script: coverage report @@ -132,7 +127,7 @@ jobs: command: 'login' - script: | - docker build -t pythondiscord/django:latest . + docker build -t pythondiscord/django:latest docker/app/Dockerfile docker push pythondiscord/django:latest displayName: Build and push the image diff --git a/docker-compose.yml b/docker-compose.yml index 8519e763..2d3e8f3b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,14 +20,14 @@ services: postgres: image: postgres:11-alpine environment: - POSTGRES_DB: pydis_site + POSTGRES_DB: pysite POSTGRES_PASSWORD: supersecretpassword - POSTGRES_USER: pydis_site + POSTGRES_USER: pysite web: build: context: . - dockerfile: docker/app/alpine/3.7/Dockerfile + dockerfile: docker/app/Dockerfile command: docker/app/migrate_and_serve.sh ports: - "127.0.0.1:8000:8000" @@ -36,7 +36,7 @@ services: volumes: - .:/app:ro environment: - DATABASE_URL: postgres://pydis_site:supersecretpassword@postgres/pydis_site + DATABASE_URL: postgres://pysite:supersecretpassword@postgres/pysite DEBUG: "true" SECRET_KEY: suitable-for-development-only diff --git a/docker/app/Dockerfile b/docker/app/Dockerfile new file mode 100644 index 00000000..93d0c378 --- /dev/null +++ b/docker/app/Dockerfile @@ -0,0 +1,42 @@ +FROM bitnami/python:3.7-prod + +STOPSIGNAL SIGQUIT +ARG EXTRAS=deploy + +RUN adduser \ + --disabled-login \ + --no-create-home \ + --uid 1500 \ + pysite + +RUN apt-get update -y \ + && \ + apt-get install --no-install-recommends -y \ + gcc \ + libc-dev \ + libpq-dev \ + && \ + apt-get clean \ + && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY Pipfile Pipfile.lock /app/ + +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 + +COPY . . + +RUN SECRET_KEY=placeholder DATABASE_URL=sqlite:// python3 manage.py collectstatic --no-input --clear --verbosity 0 + +RUN apt-get purge -y \ + gcc \ + libc-dev \ + libpq-dev + +CMD ["uwsgi", "--ini", "docker/app/uwsgi.ini"] diff --git a/docker/app/alpine/3.6/Dockerfile b/docker/app/alpine/3.6/Dockerfile deleted file mode 100644 index c40b6593..00000000 --- a/docker/app/alpine/3.6/Dockerfile +++ /dev/null @@ -1,33 +0,0 @@ -FROM python:3.6-alpine - -STOPSIGNAL SIGQUIT -ARG EXTRAS=deploy - -RUN adduser \ - -D \ - -H \ - -u 1500 \ - pysite - -RUN apk add --no-cache --virtual build \ - gcc \ - linux-headers \ - musl-dev \ - && \ - apk add --no-cache \ - curl \ - postgresql-dev - -WORKDIR /app - -COPY Pipfile /app/Pipfile -COPY Pipfile.lock /app/Pipfile.lock -RUN python3 -m pip install pipenv \ - && python3 -m pipenv install --dev --system --deploy \ - && apk del --purge build - -COPY . . - -RUN python3 manage.py collectstatic --no-input --clear - -CMD ["uwsgi", "--ini", "docker/app/uwsgi.ini"] diff --git a/docker/app/alpine/3.7/Dockerfile b/docker/app/alpine/3.7/Dockerfile deleted file mode 100644 index 9dfbfe09..00000000 --- a/docker/app/alpine/3.7/Dockerfile +++ /dev/null @@ -1,32 +0,0 @@ -FROM python:3.7-alpine - -STOPSIGNAL SIGQUIT - -RUN adduser \ - -D \ - -H \ - -u 1500 \ - pysite - -RUN apk add --no-cache --update --virtual build \ - gcc \ - linux-headers \ - musl-dev \ - && \ - apk add \ - curl \ - postgresql-dev - -WORKDIR /app - -COPY Pipfile /app/Pipfile -COPY Pipfile.lock /app/Pipfile.lock -RUN python3 -m pip install pipenv \ - && python3 -m pipenv install --dev --system --deploy \ - && apk del --purge build - -COPY . . - -RUN python3 manage.py collectstatic --no-input --clear - -CMD ["uwsgi", "--ini", "docker/app/uwsgi.ini"] diff --git a/docker/app/stretch/3.6/Dockerfile b/docker/app/stretch/3.6/Dockerfile deleted file mode 100644 index 5f97a510..00000000 --- a/docker/app/stretch/3.6/Dockerfile +++ /dev/null @@ -1,39 +0,0 @@ -FROM python:3.6-stretch - -STOPSIGNAL SIGQUIT -ARG EXTRAS=deploy - -RUN adduser \ - --disabled-login \ - --no-create-home \ - --uid 1500 \ - pysite - -RUN apt-get update -y \ - && \ - apt-get install --no-install-recommends -y \ - gcc \ - libc-dev \ - libpq-dev \ - && \ - apt-get clean \ - && \ - rm -rf /var/lib/apt/lists/* - -WORKDIR /app - -COPY Pipfile /app/Pipfile -COPY Pipfile.lock /app/Pipfile.lock -RUN python3 -m pip install pipenv \ - && python3 -m pipenv install --dev --system --deploy - -COPY . . - -RUN python3 manage.py collectstatic --no-input --clear - -RUN apt-get purge -y \ - gcc \ - libc-dev \ - libpq-dev - -CMD ["uwsgi", "--ini", "docker/app/uwsgi.ini"] diff --git a/docker/app/stretch/3.7/Dockerfile b/docker/app/stretch/3.7/Dockerfile deleted file mode 100644 index a1538ddd..00000000 --- a/docker/app/stretch/3.7/Dockerfile +++ /dev/null @@ -1,39 +0,0 @@ -FROM python:3.7-stretch - -STOPSIGNAL SIGQUIT -ARG EXTRAS=deploy - -RUN adduser \ - --disabled-login \ - --no-create-home \ - --uid 1500 \ - pysite - -RUN apt-get update -y \ - && \ - apt-get install --no-install-recommends -y \ - gcc \ - libc-dev \ - libpq-dev \ - && \ - apt-get clean \ - && \ - rm -rf /var/lib/apt/lists/* - -WORKDIR /app - -COPY Pipfile /app/Pipfile -COPY Pipfile.lock /app/Pipfile.lock -RUN python3 -m pip install pipenv \ - && python3 -m pipenv install --dev --system --deploy - -COPY . . - -RUN python3 manage.py collectstatic --no-input --clear - -RUN apt-get purge -y \ - gcc \ - libc-dev \ - libpq-dev - -CMD ["uwsgi", "--ini", "docker/app/uwsgi.ini"] diff --git a/docker/pysite.dockerapp b/docker/pysite.dockerapp index 738fcfdd..dc472b2e 100644 --- a/docker/pysite.dockerapp +++ b/docker/pysite.dockerapp @@ -1,5 +1,5 @@ version: 0.3.0 -name: pydis_site +name: pysite description: | Our community website, built on Django and PostgreSQL. namespace: python-discord 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 eecc0bc3..ea8f03d2 100644 --- a/pydis_site/apps/api/migrations/0008_tag_embed_validator.py +++ b/pydis_site/apps/api/migrations/0008_tag_embed_validator.py @@ -15,9 +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.validators.validate_tag_embed]), ), ] diff --git a/pydis_site/apps/api/migrations/0019_deletedmessage.py b/pydis_site/apps/api/migrations/0019_deletedmessage.py index 7a039675..f451ecf4 100644 --- a/pydis_site/apps/api/migrations/0019_deletedmessage.py +++ b/pydis_site/apps/api/migrations/0019_deletedmessage.py @@ -19,10 +19,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.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.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/views.py b/pydis_site/apps/api/views.py index 3160e8f7..f88e1039 100644 --- a/pydis_site/apps/api/views.py +++ b/pydis_site/apps/api/views.py @@ -131,7 +131,7 @@ class RulesView(APIView): ), ( "We will not help you with anything that might break a law or the " - "terms of service of any other community, pydis_site, service, or " + "terms of service of any other community, pysite, service, or " "otherwise - No piracy, brute-forcing, captcha circumvention, " "sneaker bots, or anything else of that nature." ), diff --git a/pydis_site/apps/api/viewsets.py b/pydis_site/apps/api/viewsets.py index 0471f79d..17024fe8 100644 --- a/pydis_site/apps/api/viewsets.py +++ b/pydis_site/apps/api/viewsets.py @@ -54,7 +54,7 @@ class DeletedMessageViewSet(CreateModelMixin, GenericViewSet): #### Body schema >>> { ... # The member ID of the original actor, if applicable. - ... # If a member ID is given, it must be present on the pydis_site. + ... # If a member ID is given, it must be present on the site. ... 'actor': Optional[int] ... 'creation': datetime, ... 'messages': [ @@ -192,7 +192,7 @@ class InfractionViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge ### 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 pydis_site. + The `actor` and `user` must be users known by the site. #### Request body >>> { @@ -427,7 +427,7 @@ class ReminderViewSet(CreateModelMixin, ListModelMixin, DestroyModelMixin, Gener 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 pydis_site. + by the bot to keep a mirror of our server's roles on the site. ## Routes ### GET /bot/roles @@ -802,7 +802,7 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): ### POST /bot/users Adds a single or multiple new users. - The roles attached to the user(s) must be roles known by the pydis_site. + The roles attached to the user(s) must be roles known by the site. #### Request body >>> { -- cgit v1.2.3 From d06fd493b865ae2936709cc0875bbb9d56e58c62 Mon Sep 17 00:00:00 2001 From: Gareth Coles Date: Sun, 7 Apr 2019 18:35:37 +0100 Subject: Address review by @jchristgit --- pydis_site/apps/api/tests/base.py | 6 +++--- pydis_site/apps/api/validators.py | 2 +- pydis_site/apps/api/viewsets.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) (limited to 'pydis_site/apps/api/viewsets.py') diff --git a/pydis_site/apps/api/tests/base.py b/pydis_site/apps/api/tests/base.py index 0290fa69..8f8ace56 100644 --- a/pydis_site/apps/api/tests/base.py +++ b/pydis_site/apps/api/tests/base.py @@ -28,7 +28,7 @@ class APISubdomainTestCase(APITestCase): If you don't want to force authentication (for example, to test a route's response for an unauthenticated user), un-force authentication by using the following: - >>> from pydis_site.apps.api import APISubdomainTestCase + >>> from pydis_site.apps.api.tests.base import APISubdomainTestCase >>> class UnauthedUserTestCase(APISubdomainTestCase): ... def setUp(self): ... super().setUp() @@ -46,7 +46,7 @@ class APISubdomainTestCase(APITestCase): ## Example Using this in a test case is rather straightforward: - >>> from pydis_site.apps.api import APISubdomainTestCase + >>> from pydis_site.apps.api.tests.base import APISubdomainTestCase >>> class MyAPITestCase(APISubdomainTestCase): ... def test_that_it_works(self): ... response = self.client.get('/my-endpoint') @@ -55,7 +55,7 @@ class APISubdomainTestCase(APITestCase): To reverse URLs of the API host, you need to use `django_hosts`: >>> from django_hosts.resolvers import reverse - >>> from pydis_site.apps.api import APISubdomainTestCase + >>> from pydis_site.apps.api.tests.base import APISubdomainTestCase >>> class MyReversedTestCase(APISubdomainTestCase): ... def test_my_endpoint(self): ... url = reverse('user-detail', host='api') diff --git a/pydis_site/apps/api/validators.py b/pydis_site/apps/api/validators.py index ac2fb739..69a8d1ef 100644 --- a/pydis_site/apps/api/validators.py +++ b/pydis_site/apps/api/validators.py @@ -86,7 +86,7 @@ def validate_tag_embed(embed): >>> from django.contrib.postgres import fields as pgfields >>> from django.db import models - >>> from pydis_site.apps.api import validate_tag_embed + >>> from pydis_site.apps.api.validators import validate_tag_embed >>> class MyMessage(models.Model): ... embed = pgfields.JSONField( ... validators=( diff --git a/pydis_site/apps/api/viewsets.py b/pydis_site/apps/api/viewsets.py index 17024fe8..949ffaaa 100644 --- a/pydis_site/apps/api/viewsets.py +++ b/pydis_site/apps/api/viewsets.py @@ -383,7 +383,7 @@ class ReminderViewSet(CreateModelMixin, ListModelMixin, DestroyModelMixin, Gener ... 'active': True, ... 'author': 1020103901030, ... 'content': "Make dinner", - ... 'expiration': '5018-11-20T15:52:00Z' + ... 'expiration': '5018-11-20T15:52:00Z', ... 'id': 11 ... }, ... ... -- cgit v1.2.3