diff options
author | 2020-03-01 12:42:18 +0100 | |
---|---|---|
committer | 2020-03-01 12:42:18 +0100 | |
commit | 36ccee0b45054de2f475735ad5b95acfa96c8cea (patch) | |
tree | 9d3722647898d9b47980c32d6b968ea288b008c2 /pydis_site | |
parent | Merge branch 'master' into deleted-messages-visible-line-endings (diff) | |
parent | Merge pull request #337 from python-discord/feat/deps/s335/wiki-pypi (diff) |
Merge branch 'master' into deleted-messages-visible-line-endings
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> |