diff options
Diffstat (limited to '')
| -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!" +            ) +        ]) | 
