diff options
Diffstat (limited to 'pydis_site')
| -rw-r--r-- | pydis_site/apps/api/migrations/0050_remove_infractions_active_default_value.py | 18 | ||||
| -rw-r--r-- | pydis_site/apps/api/models/bot/infraction.py | 1 | ||||
| -rw-r--r-- | pydis_site/apps/api/serializers.py | 2 | ||||
| -rw-r--r-- | pydis_site/apps/api/tests/test_infractions.py | 117 | ||||
| -rw-r--r-- | pydis_site/apps/api/tests/test_reminders.py | 196 | ||||
| -rw-r--r-- | pydis_site/settings.py | 34 | ||||
| -rw-r--r-- | pydis_site/static/images/sponsors/adafruit.png | bin | 7605 -> 11705 bytes | |||
| -rw-r--r-- | pydis_site/static/images/sponsors/jetbrains.png | bin | 53742 -> 177467 bytes | |||
| -rw-r--r-- | pydis_site/static/images/sponsors/sentry.png | bin | 0 -> 13895 bytes | |||
| -rw-r--r-- | pydis_site/templates/base/navbar.html | 4 | ||||
| -rw-r--r-- | pydis_site/templates/home/index.html | 15 | 
11 files changed, 349 insertions, 38 deletions
diff --git a/pydis_site/apps/api/migrations/0050_remove_infractions_active_default_value.py b/pydis_site/apps/api/migrations/0050_remove_infractions_active_default_value.py new file mode 100644 index 00000000..90c91d63 --- /dev/null +++ b/pydis_site/apps/api/migrations/0050_remove_infractions_active_default_value.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.6 on 2020-02-08 19:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + +    dependencies = [ +        ('api', '0049_deletedmessage_attachments'), +    ] + +    operations = [ +        migrations.AlterField( +            model_name='infraction', +            name='active', +            field=models.BooleanField(help_text='Whether the infraction is still active.'), +        ), +    ] diff --git a/pydis_site/apps/api/models/bot/infraction.py b/pydis_site/apps/api/models/bot/infraction.py index 108fd3a2..f58e89a3 100644 --- a/pydis_site/apps/api/models/bot/infraction.py +++ b/pydis_site/apps/api/models/bot/infraction.py @@ -29,7 +29,6 @@ class Infraction(ModelReprMixin, models.Model):          )      )      active = models.BooleanField( -        default=True,          help_text="Whether the infraction is still active."      )      user = models.ForeignKey( diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 0d1a4684..e11c4af2 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -110,7 +110,7 @@ class InfractionSerializer(ModelSerializer):          validators = [              UniqueTogetherValidator(                  queryset=Infraction.objects.filter(active=True), -                fields=['user', 'type'], +                fields=['user', 'type', 'active'],                  message='This user already has an active infraction of this type.',              )          ] diff --git a/pydis_site/apps/api/tests/test_infractions.py b/pydis_site/apps/api/tests/test_infractions.py index 7a54640e..ca87026c 100644 --- a/pydis_site/apps/api/tests/test_infractions.py +++ b/pydis_site/apps/api/tests/test_infractions.py @@ -7,6 +7,7 @@ from django_hosts.resolvers import reverse  from .base import APISubdomainTestCase  from ..models import Infraction, User +from ..serializers import InfractionSerializer  class UnauthenticatedTests(APISubdomainTestCase): @@ -54,7 +55,8 @@ class InfractionTests(APISubdomainTestCase):              type='ban',              reason='He terk my jerb!',              hidden=True, -            expires_at=dt(5018, 11, 20, 15, 52, tzinfo=timezone.utc) +            expires_at=dt(5018, 11, 20, 15, 52, tzinfo=timezone.utc), +            active=True          )          cls.ban_inactive = Infraction.objects.create(              user_id=cls.user.id, @@ -184,7 +186,8 @@ class CreationTests(APISubdomainTestCase):              'type': 'ban',              'reason': 'He terk my jerb!',              'hidden': True, -            'expires_at': '5018-11-20T15:52:00+00:00' +            'expires_at': '5018-11-20T15:52:00+00:00', +            'active': True,          }          response = self.client.post(url, data=data) @@ -208,7 +211,8 @@ class CreationTests(APISubdomainTestCase):          url = reverse('bot:infraction-list', host='api')          data = {              'actor': self.user.id, -            'type': 'kick' +            'type': 'kick', +            'active': False,          }          response = self.client.post(url, data=data) @@ -222,7 +226,8 @@ class CreationTests(APISubdomainTestCase):          data = {              'user': 1337,              'actor': self.user.id, -            'type': 'kick' +            'type': 'kick', +            'active': True,          }          response = self.client.post(url, data=data) @@ -236,7 +241,8 @@ class CreationTests(APISubdomainTestCase):          data = {              'user': self.user.id,              'actor': self.user.id, -            'type': 'hug' +            'type': 'hug', +            'active': True,          }          response = self.client.post(url, data=data) @@ -251,7 +257,8 @@ class CreationTests(APISubdomainTestCase):              'user': self.user.id,              'actor': self.user.id,              'type': 'ban', -            'expires_at': '20/11/5018 15:52:00' +            'expires_at': '20/11/5018 15:52:00', +            'active': True,          }          response = self.client.post(url, data=data) @@ -271,7 +278,8 @@ class CreationTests(APISubdomainTestCase):                  'user': self.user.id,                  'actor': self.user.id,                  'type': infraction_type, -                'expires_at': '5018-11-20T15:52:00+00:00' +                'expires_at': '5018-11-20T15:52:00+00:00', +                'active': False,              }              response = self.client.post(url, data=data) @@ -288,7 +296,8 @@ class CreationTests(APISubdomainTestCase):                  'user': self.user.id,                  'actor': self.user.id,                  'type': infraction_type, -                'hidden': True +                'hidden': True, +                'active': False,              }              response = self.client.post(url, data=data) @@ -305,6 +314,7 @@ class CreationTests(APISubdomainTestCase):              'actor': self.user.id,              'type': 'note',              'hidden': False, +            'active': False,          }          response = self.client.post(url, data=data) @@ -494,6 +504,16 @@ class CreationTests(APISubdomainTestCase):              reason="An active ban for the second user"          ) +    def test_integrity_error_if_missing_active_field(self): +        pattern = 'null value in column "active" violates not-null constraint' +        with self.assertRaisesRegex(IntegrityError, pattern): +            Infraction.objects.create( +                user=self.user, +                actor=self.user, +                type='ban', +                reason='A reason.', +            ) +  class ExpandedTests(APISubdomainTestCase):      @classmethod @@ -540,7 +560,8 @@ class ExpandedTests(APISubdomainTestCase):          data = {              'user': self.user.id,              'actor': self.user.id, -            'type': 'warning' +            'type': 'warning', +            'active': False          }          response = self.client.post(url, data=data) @@ -569,3 +590,81 @@ class ExpandedTests(APISubdomainTestCase):          infraction = Infraction.objects.get(id=self.kick.id)          self.assertEqual(infraction.active, data['active'])          self.check_expanded_fields(response.json()) + + +class SerializerTests(APISubdomainTestCase): +    @classmethod +    def setUpTestData(cls): +        cls.user = User.objects.create( +            id=5, +            name='james', +            discriminator=1, +            avatar_hash=None +        ) + +    def create_infraction(self, _type: str, active: bool): +        return Infraction.objects.create( +            user_id=self.user.id, +            actor_id=self.user.id, +            type=_type, +            reason='A reason.', +            expires_at=dt(5018, 11, 20, 15, 52, tzinfo=timezone.utc), +            active=active +        ) + +    def test_is_valid_if_active_infraction_with_same_fields_exists(self): +        self.create_infraction('ban', active=True) +        instance = self.create_infraction('ban', active=False) + +        data = {'reason': 'hello'} +        serializer = InfractionSerializer(instance, data=data, partial=True) + +        self.assertTrue(serializer.is_valid(), msg=serializer.errors) + +    def test_validation_error_if_active_duplicate(self): +        self.create_infraction('ban', active=True) +        instance = self.create_infraction('ban', active=False) + +        data = {'active': True} +        serializer = InfractionSerializer(instance, data=data, partial=True) + +        if not serializer.is_valid(): +            self.assertIn('non_field_errors', serializer.errors) + +            code = serializer.errors['non_field_errors'][0].code +            msg = f'Expected failure on unique validator but got {serializer.errors}' +            self.assertEqual(code, 'unique', msg=msg) +        else:  # pragma: no cover +            self.fail('Validation unexpectedly succeeded.') + +    def test_is_valid_for_new_active_infraction(self): +        self.create_infraction('ban', active=False) + +        data = { +            'user': self.user.id, +            'actor': self.user.id, +            'type': 'ban', +            'reason': 'A reason.', +            'active': True +        } +        serializer = InfractionSerializer(data=data) + +        self.assertTrue(serializer.is_valid(), msg=serializer.errors) + +    def test_validation_error_if_missing_active_field(self): +        data = { +            'user': self.user.id, +            'actor': self.user.id, +            'type': 'ban', +            'reason': 'A reason.', +        } +        serializer = InfractionSerializer(data=data) + +        if not serializer.is_valid(): +            self.assertIn('active', serializer.errors) + +            code = serializer.errors['active'][0].code +            msg = f'Expected failure on required active field but got {serializer.errors}' +            self.assertEqual(code, 'required', msg=msg) +        else:  # pragma: no cover +            self.fail('Validation unexpectedly succeeded.') diff --git a/pydis_site/apps/api/tests/test_reminders.py b/pydis_site/apps/api/tests/test_reminders.py new file mode 100644 index 00000000..3441e0cc --- /dev/null +++ b/pydis_site/apps/api/tests/test_reminders.py @@ -0,0 +1,196 @@ +from datetime import datetime + +from django.forms.models import model_to_dict +from django_hosts.resolvers import reverse + +from .base import APISubdomainTestCase +from ..models import Reminder, User + + +class UnauthedReminderAPITests(APISubdomainTestCase): +    def setUp(self): +        super().setUp() +        self.client.force_authenticate(user=None) + +    def test_list_returns_401(self): +        url = reverse('bot:reminder-list', host='api') +        response = self.client.get(url) + +        self.assertEqual(response.status_code, 401) + +    def test_create_returns_401(self): +        url = reverse('bot:reminder-list', host='api') +        response = self.client.post(url, data={'not': 'important'}) + +        self.assertEqual(response.status_code, 401) + +    def test_delete_returns_401(self): +        url = reverse('bot:reminder-detail', args=('1234',), host='api') +        response = self.client.delete(url) + +        self.assertEqual(response.status_code, 401) + + +class EmptyDatabaseReminderAPITests(APISubdomainTestCase): +    def test_list_all_returns_empty_list(self): +        url = reverse('bot:reminder-list', host='api') +        response = self.client.get(url) + +        self.assertEqual(response.status_code, 200) +        self.assertEqual(response.json(), []) + +    def test_delete_returns_404(self): +        url = reverse('bot:reminder-detail', args=('1234',), host='api') +        response = self.client.delete(url) + +        self.assertEqual(response.status_code, 404) + + +class ReminderCreationTests(APISubdomainTestCase): +    @classmethod +    def setUpTestData(cls): +        cls.author = User.objects.create( +            id=1234, +            name='Mermaid Man', +            discriminator=1234, +            avatar_hash=None, +        ) + +    def test_accepts_valid_data(self): +        data = { +            'author': self.author.id, +            'content': 'Remember to...wait what was it again?', +            'expiration': datetime.utcnow().isoformat(), +            'jump_url': "https://www.google.com", +            'channel_id': 123, +        } +        url = reverse('bot:reminder-list', host='api') +        response = self.client.post(url, data=data) +        self.assertEqual(response.status_code, 201) +        self.assertIsNotNone(Reminder.objects.filter(id=1).first()) + +    def test_rejects_invalid_data(self): +        data = { +            'author': self.author.id,  # Missing multiple required fields +        } +        url = reverse('bot:reminder-list', host='api') +        response = self.client.post(url, data=data) +        self.assertEqual(response.status_code, 400) +        self.assertRaises(Reminder.DoesNotExist, Reminder.objects.get, id=1) + + +class ReminderDeletionTests(APISubdomainTestCase): +    @classmethod +    def setUpTestData(cls): +        cls.author = User.objects.create( +            id=6789, +            name='Barnacle Boy', +            discriminator=6789, +            avatar_hash=None, +        ) + +        cls.reminder = Reminder.objects.create( +            author=cls.author, +            content="Don't forget to set yourself a reminder", +            expiration=datetime.utcnow().isoformat(), +            jump_url="https://www.decliningmentalfaculties.com", +            channel_id=123 +        ) + +    def test_delete_unknown_reminder_returns_404(self): +        url = reverse('bot:reminder-detail', args=('something',), host='api') +        response = self.client.delete(url) + +        self.assertEqual(response.status_code, 404) + +    def test_delete_known_reminder_returns_204(self): +        url = reverse('bot:reminder-detail', args=(self.reminder.id,), host='api') +        response = self.client.delete(url) + +        self.assertEqual(response.status_code, 204) +        self.assertRaises(Reminder.DoesNotExist, Reminder.objects.get, id=self.reminder.id) + + +class ReminderListTests(APISubdomainTestCase): +    @classmethod +    def setUpTestData(cls): +        cls.author = User.objects.create( +            id=6789, +            name='Patrick Star', +            discriminator=6789, +            avatar_hash=None, +        ) + +        cls.reminder_one = Reminder.objects.create( +            author=cls.author, +            content="We should take Bikini Bottom, and push it somewhere else!", +            expiration=datetime.utcnow().isoformat(), +            jump_url="https://www.icantseemyforehead.com", +            channel_id=123 +        ) + +        cls.reminder_two = Reminder.objects.create( +            author=cls.author, +            content="Gahhh-I love being purple!", +            expiration=datetime.utcnow().isoformat(), +            jump_url="https://www.goofygoobersicecreampartyboat.com", +            channel_id=123, +            active=False +        ) + +        cls.rem_dict_one = model_to_dict(cls.reminder_one) +        cls.rem_dict_one['expiration'] += 'Z'  # Massaging a quirk of the response time format +        cls.rem_dict_two = model_to_dict(cls.reminder_two) +        cls.rem_dict_two['expiration'] += 'Z'  # Massaging a quirk of the response time format + +    def test_reminders_in_full_list(self): +        url = reverse('bot:reminder-list', host='api') +        response = self.client.get(url) + +        self.assertEqual(response.status_code, 200) +        self.assertCountEqual(response.json(), [self.rem_dict_one, self.rem_dict_two]) + +    def test_filter_search(self): +        url = reverse('bot:reminder-list', host='api') +        response = self.client.get(f'{url}?search={self.author.name}') + +        self.assertEqual(response.status_code, 200) +        self.assertCountEqual(response.json(), [self.rem_dict_one, self.rem_dict_two]) + +    def test_filter_field(self): +        url = reverse('bot:reminder-list', host='api') +        response = self.client.get(f'{url}?active=true') + +        self.assertEqual(response.status_code, 200) +        self.assertEqual(response.json(), [self.rem_dict_one]) + + +class ReminderUpdateTests(APISubdomainTestCase): +    @classmethod +    def setUpTestData(cls): +        cls.author = User.objects.create( +            id=666, +            name='Man Ray', +            discriminator=666, +            avatar_hash=None, +        ) + +        cls.reminder = Reminder.objects.create( +            author=cls.author, +            content="Squash those do-gooders", +            expiration=datetime.utcnow().isoformat(), +            jump_url="https://www.decliningmentalfaculties.com", +            channel_id=123 +        ) + +        cls.data = {'content': 'Oops I forgot'} + +    def test_patch_updates_record(self): +        url = reverse('bot:reminder-detail', args=(self.reminder.id,), host='api') +        response = self.client.patch(url, data=self.data) + +        self.assertEqual(response.status_code, 200) +        self.assertEqual( +            Reminder.objects.filter(id=self.reminder.id).first().content, +            self.data['content'] +        ) diff --git a/pydis_site/settings.py b/pydis_site/settings.py index 72cc0ab9..5f80a414 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -16,14 +16,24 @@ import sys  import typing  import environ +import sentry_sdk  from django.contrib.messages import constants as messages +from sentry_sdk.integrations.django import DjangoIntegration +  if typing.TYPE_CHECKING:      from django.contrib.auth.models import User      from wiki.models import Article  env = environ.Env( -    DEBUG=(bool, False) +    DEBUG=(bool, False), +    SITE_SENTRY_DSN=(str, "") +) + +sentry_sdk.init( +    dsn=env('SITE_SENTRY_DSN'), +    integrations=[DjangoIntegration()], +    send_default_pii=True  )  # Build paths inside the project like this: os.path.join(BASE_DIR, ...) @@ -197,7 +207,7 @@ STATICFILES_DIRS = [os.path.join(BASE_DIR, 'pydis_site', 'static')]  STATIC_ROOT = env('STATIC_ROOT', default='/app/staticfiles')  MEDIA_URL = '/media/' -MEDIA_ROOT = env('MEDIA_ROOT', default='/app/media') +MEDIA_ROOT = env('MEDIA_ROOT', default='/site/media')  STATICFILES_FINDERS = [      'django.contrib.staticfiles.finders.FileSystemFinder', @@ -360,25 +370,7 @@ WIKI_MESSAGE_TAG_CSS_CLASS = {      messages.WARNING: "is-warning",  } -WIKI_MARKDOWN_HTML_STYLES = [ -    'max-width', -    'min-width', -    'margin', -    'padding', -    'width', -    'height', -] - -WIKI_MARKDOWN_HTML_ATTRIBUTES = { -    'img': ['class', 'id', 'src', 'alt', 'width', 'height'], -    'section': ['class', 'id'], -    'article': ['class', 'id'], -    'iframe': ['width', 'height', 'src', 'frameborder', 'allow', 'allowfullscreen'], -} - -WIKI_MARKDOWN_HTML_WHITELIST = [ -    'article', 'section', 'button', 'iframe' -] +WIKI_MARKDOWN_SANITIZE_HTML = False  # Wiki permissions diff --git a/pydis_site/static/images/sponsors/adafruit.png b/pydis_site/static/images/sponsors/adafruit.png Binary files differindex 27cd9953..eb14cf5d 100644 --- a/pydis_site/static/images/sponsors/adafruit.png +++ b/pydis_site/static/images/sponsors/adafruit.png diff --git a/pydis_site/static/images/sponsors/jetbrains.png b/pydis_site/static/images/sponsors/jetbrains.png Binary files differindex 0b21c2c8..b79e110a 100644 --- a/pydis_site/static/images/sponsors/jetbrains.png +++ b/pydis_site/static/images/sponsors/jetbrains.png diff --git a/pydis_site/static/images/sponsors/sentry.png b/pydis_site/static/images/sponsors/sentry.png Binary files differnew file mode 100644 index 00000000..ce185da2 --- /dev/null +++ b/pydis_site/static/images/sponsors/sentry.png diff --git a/pydis_site/templates/base/navbar.html b/pydis_site/templates/base/navbar.html index 2ba5bdd4..376dab5a 100644 --- a/pydis_site/templates/base/navbar.html +++ b/pydis_site/templates/base/navbar.html @@ -63,9 +63,9 @@          </a>          <div class="navbar-dropdown">            <a class="navbar-item" href="{% url 'wiki:get' path="resources/" %}"> -            Learning Resources +            Resources            </a> -          <a class="navbar-item" href="{% url 'wiki:get' path="tools/" %}"> +          <a class="navbar-item" href="{% url 'wiki:get' path="resources/tools/" %}">              Tools            </a>            <a class="navbar-item" href="{% url 'wiki:get' path="contributing/" %}"> diff --git a/pydis_site/templates/home/index.html b/pydis_site/templates/home/index.html index 3b150767..1ee93b10 100644 --- a/pydis_site/templates/home/index.html +++ b/pydis_site/templates/home/index.html @@ -37,11 +37,15 @@            </p>          </div> -        {# Code Jam banner #} +        {# Right column container #}          <div class="column is-half-desktop video-container"> -          <a href="https://pythondiscord.com/pages/code-jams/code-jam-6/"> -            <img src="https://raw.githubusercontent.com/python-discord/branding/master/logos/logo_discord_banner/code%20jam%206%20-%20website%20banner.png"/> -          </a> +          <iframe +              width="560" +              height="315" +              src="https://www.youtube.com/embed/I97L_Y3rhvc?start=381" +              frameborder="0" +              allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen> +          </iframe>          </div>        </div> @@ -92,6 +96,9 @@            <a href="https://adafruit.com" class="column is-narrow">              <img src="{% static "images/sponsors/adafruit.png" %}" alt="Adafruit"/>            </a> +          <a href="https://sentry.io" class="column is-narrow"> +            <img src="{% static "images/sponsors/sentry.png" %}" alt="Sentry"/> +          </a>          </div>        </div>      </div>  |