diff options
author | 2019-01-09 20:52:06 +0100 | |
---|---|---|
committer | 2019-01-09 20:52:09 +0100 | |
commit | 1d67beb7938635044aff75c819a7c78dde81ac63 (patch) | |
tree | 82d9a6df135af89748ba37ccc27bf06f4e3cf106 | |
parent | Use proper default for infraction insertion date. (diff) |
Add a view returning the server rules.
Closes #171.
-rw-r--r-- | api/tests/test_rules.py | 35 | ||||
-rw-r--r-- | api/urls.py | 5 | ||||
-rw-r--r-- | api/views.py | 135 |
3 files changed, 172 insertions, 3 deletions
diff --git a/api/tests/test_rules.py b/api/tests/test_rules.py new file mode 100644 index 00000000..6552333c --- /dev/null +++ b/api/tests/test_rules.py @@ -0,0 +1,35 @@ +from django_hosts.resolvers import reverse + +from .base import APISubdomainTestCase +from ..views import RulesView + + +class HealthcheckAPITests(APISubdomainTestCase): + def setUp(self): + super().setUp() + self.client.force_authenticate(user=None) + + def test_can_access_rules_view(self): + url = reverse('rules', host='api') + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertIsInstance(response.json(), list) + + def test_link_format_query_param_produces_different_results(self): + url = reverse('rules', host='api') + markdown_links_response = self.client.get(url + '?link_format=md') + html_links_response = self.client.get(url + '?link_format=html') + self.assertNotEqual( + markdown_links_response.json(), + html_links_response.json() + ) + + def test_format_link_raises_value_error_for_invalid_target(self): + with self.assertRaises(ValueError): + RulesView._format_link("a", "b", "c") + + def test_get_returns_400_for_wrong_link_format(self): + url = reverse('rules', host='api') + response = self.client.get(url + '?link_format=unknown') + self.assertEqual(response.status_code, 400) diff --git a/api/urls.py b/api/urls.py index 7d6a4f7d..66d3fb9e 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,7 +1,7 @@ from django.urls import include, path from rest_framework.routers import DefaultRouter -from .views import HealthcheckView +from .views import HealthcheckView, RulesView from .viewsets import ( DocumentationLinkViewSet, InfractionViewSet, OffTopicChannelNameViewSet, RoleViewSet, @@ -63,5 +63,6 @@ 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('healthcheck', HealthcheckView.as_view(), name='healthcheck') + path('healthcheck', HealthcheckView.as_view(), name='healthcheck'), + path('rules', RulesView.as_view(), name='rules') ) diff --git a/api/views.py b/api/views.py index c5582ec0..6a269618 100644 --- a/api/views.py +++ b/api/views.py @@ -1,3 +1,4 @@ +from rest_framework.exceptions import ParseError from rest_framework.response import Response from rest_framework.views import APIView @@ -17,7 +18,7 @@ class HealthcheckView(APIView): Seems to be. ## Authentication - Does not require any authentication nor permissions.. + Does not require any authentication nor permissions. """ authentication_classes = () @@ -25,3 +26,135 @@ class HealthcheckView(APIView): def get(self, request, format=None): # noqa return Response({'status': 'ok'}) + + +class RulesView(APIView): + """ + Return a list of the server's rules. + + ## Routes + ### GET /rules + Returns a JSON array containing the server's rules: + + >>> [ + ... "Eat candy.", + ... "Wake up at 4 AM.", + ... "Take your medicine." + ... ] + + Since some of the the rules require links, this view + gives you the option to return rules in either Markdown + or HTML format by specifying the `format`. + + ## Authentication + Does not require any authentication nor permissions. + """ + + authentication_classes = () + permission_classes = () + + @staticmethod + def _format_link(description, link, target): + """ + Build the markup necessary to render `link` with `description` + as its description in the given `target` language. + + Arguments: + description (str): + A textual description of the string. Represents the content + between the `<a>` tags in HTML, or the content between the + array brackets in Markdown. + + link (str): + The resulting link that a user should be redirected to + upon clicking the generated element. + + target (str): + One of `{'md', 'html'}`, denoting the target format that the + link should be rendered in. + + Returns: + str: + The link, rendered appropriately for the given `target` format + using `description` as its textual description. + + Raises: + ValueError: + If `target` is not `'md'` or `'html'`. + """ + + if target == 'html': + return f'<a href="{link}">{description}</a>' + elif target == 'md': + return f'[{description}]({link})' + else: + raise ValueError( + f"Can only template links to `html` or `md`, got `{target}`" + ) + + + # `format` here is the result format, we have a link format here instead. + def get(self, request, format=None): # noqa + link_format = request.query_params.get('link_format', 'md') + if link_format not in ('html', 'md'): + raise ParseError( + f"`format` must be `html` or `md`, got `{format}`." + ) + + discord_community_guidelines_link = self._format_link( + 'Discord Community Guidelines', + 'https://discordapp.com/guidelines', + link_format + ) + channels_page_link = self._format_link( + 'channels page', + 'https://pythondiscord.com/about/channels', + link_format + ) + google_translate_link = self._format_link( + 'Google Translate', + 'https://translate.google.com/', + link_format + ) + + return Response([ + "Be polite, and do not spam.", + f"Follow the {discord_community_guidelines_link}.", + ( + "Don't intentionally make other people uncomfortable - if " + "someone asks you to stop discussing something, you should stop." + ), + ( + "Be patient both with users asking " + "questions, and the users answering them." + ), + ( + "We will not help you with anything that might break a law or the " + "terms of service of any other community, site, service, or " + "otherwise - No piracy, brute-forcing, captcha circumvention, " + "sneaker bots, or anything else of that nature." + ), + ( + "Listen to and respect the staff members - we're " + "here to help, but we're all human beings." + ), + ( + "All discussion should be kept within the relevant " + "channels for the subject - See the " + f"{channels_page_link} for more information." + ), + ( + "This is an English-speaking server, so please speak English " + f"to the best of your ability - {google_translate_link} " + "should be fine if you're not sure." + ), + ( + "Keep all discussions safe for work - No gore, nudity, sexual " + "soliciting, references to suicide, or anything else of that nature" + ), + ( + "We do not allow advertisements for communities (including " + "other Discord servers) or commercial projects - Contact " + "us directly if you want to discuss a partnership!" + ) + ]) |