From 6ab05d33b750afe069f399b801ebd424b5d98a15 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sun, 20 Oct 2019 05:14:49 +1000 Subject: Set default example.com dev site to pythondiscord.local:8000 --- manage.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/manage.py b/manage.py index d2996488..dc4aef08 100755 --- a/manage.py +++ b/manage.py @@ -112,6 +112,19 @@ class SiteManager: print("Database could not be found, exiting.") sys.exit(1) + @staticmethod + def set_dev_site_name() -> None: + """Set the development site domain in admin from default example.""" + # import Site model now after django setup + from django.contrib.sites.models import Site + query = Site.objects.filter(id=1) + site = query.get() + if site.domain == "example.com": + query.update( + domain="pythondiscord.local:8000", + name="pythondiscord.local:8000" + ) + def prepare_server(self) -> None: """Perform preparation tasks before running the server.""" django.setup() @@ -125,6 +138,7 @@ class SiteManager: call_command("collectstatic", interactive=False, clear=True, verbosity=self.verbosity) if self.debug: + self.set_dev_site_name() self.create_superuser() def run_server(self) -> None: -- cgit v1.2.3 From e2956b87289a37747cb5431ad3a08dc202a2bcba Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sun, 20 Oct 2019 05:37:15 +1000 Subject: Set newest-first sorting for message deletion models, add log_url property. --- pydis_site/apps/api/models/bot/deleted_message.py | 4 ++-- pydis_site/apps/api/models/bot/message_deletion_context.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/pydis_site/apps/api/models/bot/deleted_message.py b/pydis_site/apps/api/models/bot/deleted_message.py index 1eb4516e..50b70d8c 100644 --- a/pydis_site/apps/api/models/bot/deleted_message.py +++ b/pydis_site/apps/api/models/bot/deleted_message.py @@ -14,6 +14,6 @@ class DeletedMessage(Message): ) class Meta: - """Sets the default ordering for list views to oldest first.""" + """Sets the default ordering for list views to newest first.""" - ordering = ["id"] + ordering = ("-id",) diff --git a/pydis_site/apps/api/models/bot/message_deletion_context.py b/pydis_site/apps/api/models/bot/message_deletion_context.py index 44a0c8ae..02a15ca0 100644 --- a/pydis_site/apps/api/models/bot/message_deletion_context.py +++ b/pydis_site/apps/api/models/bot/message_deletion_context.py @@ -1,3 +1,4 @@ +from django.contrib.sites.models import Site from django.db import models from pydis_site.apps.api.models.bot.user import User @@ -28,3 +29,14 @@ class MessageDeletionContext(ModelReprMixin, models.Model): # the deletion context does not take place in the future. help_text="When this deletion took place." ) + + @property + def log_url(self) -> str: + """Create the url for the deleted message logs.""" + domain = Site.objects.get_current().domain + return f"http://staff.{domain}/bot/logs/{self.id}/" + + class Meta: + """Set the ordering for list views to newest first.""" + + ordering = ("-creation",) -- cgit v1.2.3 From f590d2bc5960cafede1e596d67028052f2fad5b2 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sun, 20 Oct 2019 05:39:11 +1000 Subject: Add message log links, improved formatting to message deletion admin pages. --- pydis_site/apps/api/admin.py | 82 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 7 deletions(-) diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index 059f52eb..6d6a9b3b 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -1,7 +1,10 @@ +import json from typing import Optional +from django import urls from django.contrib import admin from django.http import HttpRequest +from django.utils.html import format_html from .models import ( BotSetting, @@ -44,21 +47,86 @@ class LogEntryAdmin(admin.ModelAdmin): """Deny manual LogEntry creation.""" return False - def has_delete_permission( - self, - request: HttpRequest, - obj: Optional[LogEntry] = None - ) -> bool: + def has_delete_permission(self, request: HttpRequest, obj: Optional[LogEntry] = None) -> bool: """Deny LogEntry deletion.""" return False +class DeletedMessageAdmin(admin.ModelAdmin): + """Admin formatting for the DeletedMessage model.""" + + readonly_fields = ( + "id", + "author", + "channel_id", + "content", + "embed_data", + "context", + "view_full_log" + ) + + exclude = ("embeds", "deletion_context") + + search_fields = ( + "id", + "content", + "author__name", + "author__id", + "deletion_context__actor__name", + "deletion_context__actor__id" + ) + + @staticmethod + def embed_data(instance: DeletedMessage) -> Optional[str]: + """Format embed data in a code block for better readability.""" + if instance.embeds: + return format_html( + "
{0}
", + json.dumps(instance.embeds, indent=4) + ) + + @staticmethod + def context(instance: DeletedMessage) -> str: + """Provide full context info with a link through to context admin view.""" + link = urls.reverse( + "admin:api_messagedeletioncontext_change", + args=[instance.deletion_context.id] + ) + details = ( + f"Deleted by {instance.deletion_context.actor} at " + f"{instance.deletion_context.creation}" + ) + return format_html("{1}", link, details) + + @staticmethod + def view_full_log(instance: DeletedMessage) -> str: + """Provide a link to the message logs for the relevant context.""" + return format_html( + "Click to view full context log", + instance.deletion_context.log_url + ) + + +class MessageDeletionContextAdmin(admin.ModelAdmin): + """Admin formatting for the MessageDeletionContext model.""" + + readonly_fields = ("actor", "creation", "message_log") + + @staticmethod + def message_log(instance: MessageDeletionContext) -> str: + """Provide a formatted link to the message logs for the context.""" + return format_html( + "Click to see deleted message log", + instance.log_url + ) + + admin.site.register(BotSetting) -admin.site.register(DeletedMessage) +admin.site.register(DeletedMessage, DeletedMessageAdmin) admin.site.register(DocumentationLink) admin.site.register(Infraction) admin.site.register(LogEntry, LogEntryAdmin) -admin.site.register(MessageDeletionContext) +admin.site.register(MessageDeletionContext, MessageDeletionContextAdmin) admin.site.register(Nomination) admin.site.register(OffTopicChannelName) admin.site.register(Role) -- cgit v1.2.3 From 618610fe367b0c8d6175d251c276c2e37db8aa52 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sun, 20 Oct 2019 05:43:21 +1000 Subject: Order roles by positioning, add filters and search to api user admin page. --- pydis_site/apps/api/admin.py | 45 ++++++++++++++++++++++++++++++++-- pydis_site/apps/api/models/bot/role.py | 5 ++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index 6d6a9b3b..65cc0a6c 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -1,8 +1,9 @@ import json -from typing import Optional +from typing import Optional, Tuple from django import urls from django.contrib import admin +from django.db.models.query import QuerySet from django.http import HttpRequest from django.utils.html import format_html @@ -121,6 +122,46 @@ class MessageDeletionContextAdmin(admin.ModelAdmin): ) +class StaffRolesFilter(admin.SimpleListFilter): + """Filter options for Staff Roles.""" + + title = "Staff Role" + parameter_name = "staff_role" + + @staticmethod + def lookups(*_) -> Tuple[Tuple[str, str], ...]: + """Available filter options.""" + return ( + ("Owners", "Owners"), + ("Admins", "Admins"), + ("Moderators", "Moderators"), + ("Core Developers", "Core Developers"), + ("Helpers", "Helpers"), + ) + + def queryset(self, request: HttpRequest, queryset: QuerySet) -> Optional[QuerySet]: + """Returned data filter based on selected option.""" + value = self.value() + if value: + return queryset.filter(roles__name=value) + + +class UserAdmin(admin.ModelAdmin): + """Admin formatting for the User model.""" + + search_fields = ("name", "id", "roles__name", "roles__id") + list_filter = ("in_guild", StaffRolesFilter) + exclude = ("name", "discriminator") + readonly_fields = ( + "__str__", + "id", + "avatar_hash", + "top_role", + "roles", + "in_guild", + ) + + admin.site.register(BotSetting) admin.site.register(DeletedMessage, DeletedMessageAdmin) admin.site.register(DocumentationLink) @@ -131,4 +172,4 @@ admin.site.register(Nomination) admin.site.register(OffTopicChannelName) admin.site.register(Role) admin.site.register(Tag) -admin.site.register(User) +admin.site.register(User, UserAdmin) diff --git a/pydis_site/apps/api/models/bot/role.py b/pydis_site/apps/api/models/bot/role.py index 58bbf8b4..b95740da 100644 --- a/pydis_site/apps/api/models/bot/role.py +++ b/pydis_site/apps/api/models/bot/role.py @@ -65,3 +65,8 @@ class Role(ModelReprMixin, models.Model): def __le__(self, other: Role) -> bool: """Compares the roles based on their position in the role hierarchy of the guild.""" return self.position <= other.position + + class Meta: + """Set role ordering from highest to lowest position.""" + + ordering = ("-position",) -- cgit v1.2.3 From 0021d4a2906021351203ffabc73d6e02ecf400c4 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sun, 20 Oct 2019 06:21:13 +1000 Subject: Improve infractions admin list and page, add search and filters. --- pydis_site/apps/api/admin.py | 46 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index 65cc0a6c..74b9413b 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -122,6 +122,50 @@ class MessageDeletionContextAdmin(admin.ModelAdmin): ) +class InfractionAdmin(admin.ModelAdmin): + """Admin formatting for the Infraction model.""" + + fields = ( + "user", + "actor", + "type", + "reason", + "inserted_at", + "expires_at", + "active", + "hidden" + ) + readonly_fields = ( + "user", + "actor", + "type", + "inserted_at" + ) + list_display = ( + "type", + "user", + "actor", + "inserted_at", + "expires_at", + "reason", + "active", + ) + search_fields = ( + "id", + "user__name", + "user__id", + "actor__name", + "actor__id", + "reason", + "type" + ) + list_filter = ( + "type", + "hidden", + "active" + ) + + class StaffRolesFilter(admin.SimpleListFilter): """Filter options for Staff Roles.""" @@ -165,7 +209,7 @@ class UserAdmin(admin.ModelAdmin): admin.site.register(BotSetting) admin.site.register(DeletedMessage, DeletedMessageAdmin) admin.site.register(DocumentationLink) -admin.site.register(Infraction) +admin.site.register(Infraction, InfractionAdmin) admin.site.register(LogEntry, LogEntryAdmin) admin.site.register(MessageDeletionContext, MessageDeletionContextAdmin) admin.site.register(Nomination) -- cgit v1.2.3 From 1c41c8a1aa07d7a716561c7594f769db7aa58cf5 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sun, 20 Oct 2019 06:33:35 +1000 Subject: Improve nominations admin list and page, add search and filter by active. --- pydis_site/apps/api/admin.py | 39 +++++++++++++++++++++++++++- pydis_site/apps/api/models/bot/nomination.py | 5 ++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index 74b9413b..55a9d655 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -166,6 +166,43 @@ class InfractionAdmin(admin.ModelAdmin): ) +class NominationAdmin(admin.ModelAdmin): + """Admin formatting for the Nomination model.""" + + list_display = ( + "user", + "active", + "reason", + "actor", + "inserted_at", + "ended_at" + ) + fields = ( + "user", + "active", + "actor", + "reason", + "inserted_at", + "ended_at", + "end_reason" + ) + readonly_fields = ( + "user", + "active", + "actor", + "inserted_at", + "ended_at" + ) + search_fields = ( + "actor__name", + "actor__id", + "user__name", + "user__id", + "reason" + ) + list_filter = ("active",) + + class StaffRolesFilter(admin.SimpleListFilter): """Filter options for Staff Roles.""" @@ -212,7 +249,7 @@ admin.site.register(DocumentationLink) admin.site.register(Infraction, InfractionAdmin) admin.site.register(LogEntry, LogEntryAdmin) admin.site.register(MessageDeletionContext, MessageDeletionContextAdmin) -admin.site.register(Nomination) +admin.site.register(Nomination, NominationAdmin) admin.site.register(OffTopicChannelName) admin.site.register(Role) admin.site.register(Tag) diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py index cd9951aa..a0ba42a3 100644 --- a/pydis_site/apps/api/models/bot/nomination.py +++ b/pydis_site/apps/api/models/bot/nomination.py @@ -44,3 +44,8 @@ class Nomination(ModelReprMixin, models.Model): """Representation that makes the target and state of the nomination immediately evident.""" status = "active" if self.active else "ended" return f"Nomination of {self.user} ({status})" + + class Meta: + """Set the ordering of nominations to most recent first.""" + + ordering = ("-inserted_at",) -- cgit v1.2.3 From 77da3336a248a5a01fe1f83493e0424dbd261cd2 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sun, 20 Oct 2019 06:37:55 +1000 Subject: Add search field to off topic admin page. --- pydis_site/apps/api/admin.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index 55a9d655..fe0e3235 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -203,6 +203,12 @@ class NominationAdmin(admin.ModelAdmin): list_filter = ("active",) +class OffTopicChannelNameAdmin(admin.ModelAdmin): + """Admin formatting for the OffTopicChannelName model.""" + + search_fields = ("name",) + + class StaffRolesFilter(admin.SimpleListFilter): """Filter options for Staff Roles.""" @@ -250,7 +256,7 @@ admin.site.register(Infraction, InfractionAdmin) admin.site.register(LogEntry, LogEntryAdmin) admin.site.register(MessageDeletionContext, MessageDeletionContextAdmin) admin.site.register(Nomination, NominationAdmin) -admin.site.register(OffTopicChannelName) +admin.site.register(OffTopicChannelName, OffTopicChannelNameAdmin) admin.site.register(Role) admin.site.register(Tag) admin.site.register(User, UserAdmin) -- cgit v1.2.3 From 72f156fda828a7e28605d0fe07bd990d05f61925 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sun, 20 Oct 2019 07:18:42 +1000 Subject: Show role colour style and add hex value, link perms to calc page, add role search. --- pydis_site/apps/api/admin.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index fe0e3235..237d68a4 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -209,6 +209,38 @@ class OffTopicChannelNameAdmin(admin.ModelAdmin): search_fields = ("name",) +class RoleAdmin(admin.ModelAdmin): + """Admin formatting for the Role model.""" + + exclude = ("permissions", "colour") + readonly_fields = ( + "name", + "id", + "colour_with_preview", + "permissions_with_calc_link", + "position" + ) + search_fields = ("name", "id") + + def colour_with_preview(self, instance: Role) -> str: + """Show colour value in both int and hex, in bolded and coloured style.""" + return format_html( + "{1} / #{0}", + f"{instance.colour:06x}", + instance.colour + ) + + def permissions_with_calc_link(self, instance: Role) -> str: + """Show permissions with link to API permissions calculator page.""" + return format_html( + "{0}", + instance.permissions + ) + + colour_with_preview.short_description = "Colour" + permissions_with_calc_link.short_description = "Permissions" + + class StaffRolesFilter(admin.SimpleListFilter): """Filter options for Staff Roles.""" @@ -257,6 +289,6 @@ admin.site.register(LogEntry, LogEntryAdmin) admin.site.register(MessageDeletionContext, MessageDeletionContextAdmin) admin.site.register(Nomination, NominationAdmin) admin.site.register(OffTopicChannelName, OffTopicChannelNameAdmin) -admin.site.register(Role) +admin.site.register(Role, RoleAdmin) admin.site.register(Tag) admin.site.register(User, UserAdmin) -- cgit v1.2.3 From e19d34b06f6485d2809b600547ae5b672c31fe7e Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sun, 20 Oct 2019 07:50:00 +1000 Subject: Add tag search and rendered preview. --- pydis_site/apps/api/admin.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index 237d68a4..010541a6 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -82,7 +82,8 @@ class DeletedMessageAdmin(admin.ModelAdmin): """Format embed data in a code block for better readability.""" if instance.embeds: return format_html( - "
{0}
", + "
"
+                "{0}
", json.dumps(instance.embeds, indent=4) ) @@ -241,6 +242,29 @@ class RoleAdmin(admin.ModelAdmin): permissions_with_calc_link.short_description = "Permissions" +class TagAdmin(admin.ModelAdmin): + """Admin formatting for the Tag model.""" + + fields = ("title", "embed", "preview") + readonly_fields = ("preview",) + search_fields = ("title", "embed") + + @staticmethod + def preview(instance: Tag) -> Optional[str]: + """Render tag markdown contents to preview actual appearance.""" + if instance.embed: + import markdown + return format_html( + markdown.markdown( + instance.embed["description"], + extensions=[ + "markdown.extensions.nl2br", + "markdown.extensions.extra" + ] + ) + ) + + class StaffRolesFilter(admin.SimpleListFilter): """Filter options for Staff Roles.""" @@ -290,5 +314,5 @@ admin.site.register(MessageDeletionContext, MessageDeletionContextAdmin) admin.site.register(Nomination, NominationAdmin) admin.site.register(OffTopicChannelName, OffTopicChannelNameAdmin) admin.site.register(Role, RoleAdmin) -admin.site.register(Tag) +admin.site.register(Tag, TagAdmin) admin.site.register(User, UserAdmin) -- cgit v1.2.3 From b6d01478bab4a2706202d44f117498ca676c00df Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sun, 20 Oct 2019 08:51:28 +1000 Subject: Adjust deleted message test to account for new ordering of newest created first. --- pydis_site/apps/staff/tests/test_logs_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/apps/staff/tests/test_logs_view.py b/pydis_site/apps/staff/tests/test_logs_view.py index 32cb6bbf..5036363b 100644 --- a/pydis_site/apps/staff/tests/test_logs_view.py +++ b/pydis_site/apps/staff/tests/test_logs_view.py @@ -132,7 +132,7 @@ class TestLogsView(TestCase): response = self.client.get(url) self.assertIn("messages", response.context) self.assertListEqual( - [self.deleted_message_one, self.deleted_message_two], + [self.deleted_message_two, self.deleted_message_one], list(response.context["deletion_context"].deletedmessage_set.all()) ) -- cgit v1.2.3 From 56c6c85ad973a3b594d72215cdfb2d3b6c19d741 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sun, 20 Oct 2019 09:29:58 +1000 Subject: Add new test for deleted message context log_url. --- .../api/models/bot/message_deletion_context.py | 5 ++--- pydis_site/apps/api/tests/test_deleted_messages.py | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/pydis_site/apps/api/models/bot/message_deletion_context.py b/pydis_site/apps/api/models/bot/message_deletion_context.py index 02a15ca0..fde9b0a6 100644 --- a/pydis_site/apps/api/models/bot/message_deletion_context.py +++ b/pydis_site/apps/api/models/bot/message_deletion_context.py @@ -1,5 +1,5 @@ -from django.contrib.sites.models import Site from django.db import models +from django_hosts.resolvers import reverse from pydis_site.apps.api.models.bot.user import User from pydis_site.apps.api.models.utils import ModelReprMixin @@ -33,8 +33,7 @@ class MessageDeletionContext(ModelReprMixin, models.Model): @property def log_url(self) -> str: """Create the url for the deleted message logs.""" - domain = Site.objects.get_current().domain - return f"http://staff.{domain}/bot/logs/{self.id}/" + return reverse('logs', host="staff", args=(self.id,)) class Meta: """Set the ordering for list views to newest first.""" diff --git a/pydis_site/apps/api/tests/test_deleted_messages.py b/pydis_site/apps/api/tests/test_deleted_messages.py index d1e9f2f5..ccccdda4 100644 --- a/pydis_site/apps/api/tests/test_deleted_messages.py +++ b/pydis_site/apps/api/tests/test_deleted_messages.py @@ -1,5 +1,6 @@ from datetime import datetime +from django.utils import timezone from django_hosts.resolvers import reverse from .base import APISubdomainTestCase @@ -75,3 +76,24 @@ class DeletedMessagesWithActorTests(APISubdomainTestCase): self.assertEqual(response.status_code, 201) [context] = MessageDeletionContext.objects.all() self.assertEqual(context.actor.id, self.actor.id) + + +class DeletedMessagesLogURLTests(APISubdomainTestCase): + @classmethod + def setUpTestData(cls): # noqa + cls.author = cls.actor = User.objects.create( + id=324888, + name='Black Knight', + discriminator=1975, + avatar_hash=None + ) + + cls.deletion_context = MessageDeletionContext.objects.create( + actor=cls.actor, + creation=timezone.now() + ) + + def test_valid_log_url(self): + expected_url = reverse('logs', host="staff", args=(1,)) + [context] = MessageDeletionContext.objects.all() + self.assertEqual(context.log_url, expected_url) -- cgit v1.2.3 From 46ef497fd028531ffac8ebc19dec41a3272f72a3 Mon Sep 17 00:00:00 2001 From: ks123 Date: Mon, 30 Mar 2020 13:30:00 +0300 Subject: (Off-topic Channel Names): Added new field to model: `used` that show is this name already used on this round of names, added migration for this. --- .../api/migrations/0051_offtopicchannelname_used.py | 18 ++++++++++++++++++ .../apps/api/models/bot/off_topic_channel_name.py | 5 +++++ 2 files changed, 23 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py diff --git a/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py new file mode 100644 index 00000000..74836d8c --- /dev/null +++ b/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.11 on 2020-03-30 10:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0050_remove_infractions_active_default_value'), + ] + + operations = [ + migrations.AddField( + model_name='offtopicchannelname', + name='used', + field=models.BooleanField(default=False, help_text='Show is channel already used as channel name in this round.'), + ), + ] diff --git a/pydis_site/apps/api/models/bot/off_topic_channel_name.py b/pydis_site/apps/api/models/bot/off_topic_channel_name.py index 29280c27..3345754d 100644 --- a/pydis_site/apps/api/models/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/models/bot/off_topic_channel_name.py @@ -16,6 +16,11 @@ class OffTopicChannelName(ModelReprMixin, models.Model): help_text="The actual channel name that will be used on our Discord server." ) + used = models.BooleanField( + default=False, + help_text="Show is channel already used as channel name in this round." + ) + def __str__(self): """Returns the current off-topic name, for display purposes.""" return self.name -- cgit v1.2.3 From a3d2938d63443ee6fa43751f42c935b22d4efb47 Mon Sep 17 00:00:00 2001 From: ks123 Date: Mon, 30 Mar 2020 17:28:44 +0300 Subject: (Off-topic Channel Names Viewset): Added documentation about new `mark_used` query parameter, added implementation of this param. --- .../api/viewsets/bot/off_topic_channel_name.py | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py index d6da2399..4328c894 100644 --- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py @@ -21,6 +21,10 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): 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. + If the `mark_used` query parameter is given like... + $ curl api.pydis.local:8000/bot/off-topic-channel-names?random_items=5&mark_used=true + ... then the API will mark returned `5` items `used`. + When running out of names, API will mark all names to not used and start new round. #### Response format Return a list of off-topic-channel names: @@ -106,6 +110,31 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): 'random_items': ["Must be a positive integer."] }) + if 'mark_used' in request.query_params and request.query_params['mark_used']: + queryset = self.get_queryset().order_by('?').exclude(used=True)[:random_count] + self.get_queryset().filter( + name__in=(query.name for query in queryset) + ).update(used=True) + + # When client request more channel names than non-used names is available, start + # new round of names. + if len(queryset) < random_count: + # Get how much names still missing and don't fetch duplicate names. + need_more = random_count - len(queryset) + ext = self.get_queryset().order_by('?').exclude( + name__in=(query.name for query in queryset) + )[:need_more] + + # Set all names `used` field to False except these that we just used. + self.get_queryset().exclude(name__in=( + query.name for query in ext) + ).update(used=False) + # Join original queryset (that had missing names) + # and extension with these missing names. + queryset = list(queryset) + list(ext) + serialized = self.serializer_class(queryset, many=True) + return Response(serialized.data) + queryset = self.get_queryset().order_by('?')[:random_count] serialized = self.serializer_class(queryset, many=True) return Response(serialized.data) -- cgit v1.2.3 From 81516be86a22ba1ea6df419f0e4b4277adecae09 Mon Sep 17 00:00:00 2001 From: ks123 Date: Tue, 31 Mar 2020 13:44:38 +0300 Subject: (Off-topic Channel Names Viewset Tests): Added test for not authenticated request. --- pydis_site/apps/api/tests/test_off_topic_channel_names.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pydis_site/apps/api/tests/test_off_topic_channel_names.py b/pydis_site/apps/api/tests/test_off_topic_channel_names.py index bd42cd81..9697f1f6 100644 --- a/pydis_site/apps/api/tests/test_off_topic_channel_names.py +++ b/pydis_site/apps/api/tests/test_off_topic_channel_names.py @@ -21,6 +21,12 @@ class UnauthenticatedTests(APISubdomainTestCase): self.assertEqual(response.status_code, 401) + def test_cannot_read_off_topic_channel_name_list_with_random_item_and_mark_used_param(self): + url = reverse('bot:offtopicchannelname-list', host='api') + response = self.client.get(f'{url}?random_items=no&mark_used=true') + + self.assertEqual(response.status_code, 401) + class EmptyDatabaseTests(APISubdomainTestCase): def test_returns_empty_object(self): -- cgit v1.2.3 From 1f66694835013b1025a1630afe3290adbc03b94f Mon Sep 17 00:00:00 2001 From: ks123 Date: Tue, 31 Mar 2020 13:45:46 +0300 Subject: (Off-topic Channel Names Viewset Tests): Added used parameter to list tests. --- pydis_site/apps/api/tests/test_off_topic_channel_names.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pydis_site/apps/api/tests/test_off_topic_channel_names.py b/pydis_site/apps/api/tests/test_off_topic_channel_names.py index 9697f1f6..fea3932b 100644 --- a/pydis_site/apps/api/tests/test_off_topic_channel_names.py +++ b/pydis_site/apps/api/tests/test_off_topic_channel_names.py @@ -65,8 +65,8 @@ class EmptyDatabaseTests(APISubdomainTestCase): class ListTests(APISubdomainTestCase): @classmethod def setUpTestData(cls): - cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand') - cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk') + cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand', used=False) + cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk', used=True) def test_returns_name_in_list(self): url = reverse('bot:offtopicchannelname-list', host='api') -- cgit v1.2.3 From 02315e7339858ae6a11bc742446a14b7220894ca Mon Sep 17 00:00:00 2001 From: ks123 Date: Tue, 31 Mar 2020 13:50:39 +0300 Subject: (Off-topic Channel Names Viewset Tests): Added test for default handling `mark_used` parameter. --- pydis_site/apps/api/tests/test_off_topic_channel_names.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pydis_site/apps/api/tests/test_off_topic_channel_names.py b/pydis_site/apps/api/tests/test_off_topic_channel_names.py index fea3932b..be0d5001 100644 --- a/pydis_site/apps/api/tests/test_off_topic_channel_names.py +++ b/pydis_site/apps/api/tests/test_off_topic_channel_names.py @@ -88,6 +88,13 @@ class ListTests(APISubdomainTestCase): self.assertEqual(response.status_code, 200) self.assertEqual(len(response.json()), 1) + def test_returns_single_correct_item_with_mark_used_parameter_true_and_random_items_1(self): + url = reverse('bot:offtopicchannelname-list', host='api') + response = self.client.get(f'{url}?random_items=1&mark_used=true') + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), [self.test_name.name]) + class CreationTests(APISubdomainTestCase): def setUp(self): -- cgit v1.2.3 From d6f657143fa38657fb0432b9adb779969e920c64 Mon Sep 17 00:00:00 2001 From: ks123 Date: Tue, 31 Mar 2020 13:54:59 +0300 Subject: (Off-topic Channel Names Viewset Tests): Added test for handling running out of names. --- pydis_site/apps/api/tests/test_off_topic_channel_names.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pydis_site/apps/api/tests/test_off_topic_channel_names.py b/pydis_site/apps/api/tests/test_off_topic_channel_names.py index be0d5001..06624d89 100644 --- a/pydis_site/apps/api/tests/test_off_topic_channel_names.py +++ b/pydis_site/apps/api/tests/test_off_topic_channel_names.py @@ -95,6 +95,13 @@ class ListTests(APISubdomainTestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), [self.test_name.name]) + def test_running_out_of_names_with_mark_used_parameter(self): + url = reverse('bot:offtopicchannelname-list', host='api') + response = self.client.get(f'{url}?random_items=2&mark_used=true') + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), [self.test_name.name, self.test_name_2.name]) + class CreationTests(APISubdomainTestCase): def setUp(self): -- cgit v1.2.3 From e321a25c05875e82073470429708b37849947b16 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 28 May 2020 08:23:14 +0300 Subject: OT: Replace help text of `used` field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Leon Sandรธy --- pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py | 2 +- pydis_site/apps/api/models/bot/off_topic_channel_name.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py index 74836d8c..1b838aec 100644 --- a/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py +++ b/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py @@ -13,6 +13,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='offtopicchannelname', name='used', - field=models.BooleanField(default=False, help_text='Show is channel already used as channel name in this round.'), + field=models.BooleanField(default=False, help_text='Whether or not this name has already been used during this rotation'), ), ] diff --git a/pydis_site/apps/api/models/bot/off_topic_channel_name.py b/pydis_site/apps/api/models/bot/off_topic_channel_name.py index 3345754d..413cbfae 100644 --- a/pydis_site/apps/api/models/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/models/bot/off_topic_channel_name.py @@ -18,7 +18,7 @@ class OffTopicChannelName(ModelReprMixin, models.Model): used = models.BooleanField( default=False, - help_text="Show is channel already used as channel name in this round." + help_text="Whether or not this name has already been used during this rotation", ) def __str__(self): -- cgit v1.2.3 From 1e3501b2f27d7e09a873dc3348d196ac2edda68b Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 28 May 2020 08:49:49 +0300 Subject: OT: Fix migrations nr conflict --- .../api/migrations/0051_offtopicchannelname_used.py | 18 ------------------ .../api/migrations/0052_offtopicchannelname_used.py | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 18 deletions(-) delete mode 100644 pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py create mode 100644 pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py diff --git a/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py deleted file mode 100644 index 1b838aec..00000000 --- a/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.11 on 2020-03-30 10:24 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0050_remove_infractions_active_default_value'), - ] - - operations = [ - migrations.AddField( - model_name='offtopicchannelname', - name='used', - field=models.BooleanField(default=False, help_text='Whether or not this name has already been used during this rotation'), - ), - ] diff --git a/pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py new file mode 100644 index 00000000..dfdf3835 --- /dev/null +++ b/pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.11 on 2020-03-30 10:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0051_create_news_setting'), + ] + + operations = [ + migrations.AddField( + model_name='offtopicchannelname', + name='used', + field=models.BooleanField(default=False, help_text='Whether or not this name has already been used during this rotation'), + ), + ] -- cgit v1.2.3 From 7d5e1f9c60007b230fecdb2b649c5574462fdeb1 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 28 May 2020 09:06:21 +0300 Subject: OT: Refactor off-topic-names random items getting Remove `mark_used` parameter and move this functionality to `random_items` parameter. Update docstring of class --- .../api/viewsets/bot/off_topic_channel_name.py | 55 ++++++++++------------ 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py index 4328c894..e6cf8172 100644 --- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py @@ -20,11 +20,9 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): 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. - If the `mark_used` query parameter is given like... - $ curl api.pydis.local:8000/bot/off-topic-channel-names?random_items=5&mark_used=true - ... then the API will mark returned `5` items `used`. - When running out of names, API will mark all names to not used and start new round. + ... then the API will return `5` random items from the database + that is not used in current rotation. + When running out of names, API will mark all names to not used and start new rotation. #### Response format Return a list of off-topic-channel names: @@ -110,32 +108,29 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): 'random_items': ["Must be a positive integer."] }) - if 'mark_used' in request.query_params and request.query_params['mark_used']: - queryset = self.get_queryset().order_by('?').exclude(used=True)[:random_count] - self.get_queryset().filter( + queryset = self.get_queryset().order_by('?').exclude(used=True)[:random_count] + self.get_queryset().filter( + name__in=(query.name for query in queryset) + ).update(used=True) + + # When client request more channel names than non-used names is available, start + # new round of names. + if len(queryset) < random_count: + # Get how much names still missing and don't fetch duplicate names. + need_more = random_count - len(queryset) + ext = self.get_queryset().order_by('?').exclude( name__in=(query.name for query in queryset) - ).update(used=True) - - # When client request more channel names than non-used names is available, start - # new round of names. - if len(queryset) < random_count: - # Get how much names still missing and don't fetch duplicate names. - need_more = random_count - len(queryset) - ext = self.get_queryset().order_by('?').exclude( - name__in=(query.name for query in queryset) - )[:need_more] - - # Set all names `used` field to False except these that we just used. - self.get_queryset().exclude(name__in=( - query.name for query in ext) - ).update(used=False) - # Join original queryset (that had missing names) - # and extension with these missing names. - queryset = list(queryset) + list(ext) - serialized = self.serializer_class(queryset, many=True) - return Response(serialized.data) - - queryset = self.get_queryset().order_by('?')[:random_count] + )[:need_more] + + # Set all names `used` field to False except these that we just used. + self.get_queryset().exclude(name__in=( + query.name for query in ext) + ).update(used=False) + + # Join original queryset (that had missing names) + # and extension with these missing names. + queryset = list(queryset) + list(ext) + serialized = self.serializer_class(queryset, many=True) return Response(serialized.data) -- cgit v1.2.3 From 8584aa2a409922c60b9c4a8e0874bc92cd01b9a1 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 28 May 2020 09:12:03 +0300 Subject: OT Tests: Refactor tests to latest change --- .../apps/api/tests/test_off_topic_channel_names.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/pydis_site/apps/api/tests/test_off_topic_channel_names.py b/pydis_site/apps/api/tests/test_off_topic_channel_names.py index 06624d89..51372651 100644 --- a/pydis_site/apps/api/tests/test_off_topic_channel_names.py +++ b/pydis_site/apps/api/tests/test_off_topic_channel_names.py @@ -21,12 +21,6 @@ class UnauthenticatedTests(APISubdomainTestCase): self.assertEqual(response.status_code, 401) - def test_cannot_read_off_topic_channel_name_list_with_random_item_and_mark_used_param(self): - url = reverse('bot:offtopicchannelname-list', host='api') - response = self.client.get(f'{url}?random_items=no&mark_used=true') - - self.assertEqual(response.status_code, 401) - class EmptyDatabaseTests(APISubdomainTestCase): def test_returns_empty_object(self): @@ -87,17 +81,11 @@ class ListTests(APISubdomainTestCase): self.assertEqual(response.status_code, 200) self.assertEqual(len(response.json()), 1) - - def test_returns_single_correct_item_with_mark_used_parameter_true_and_random_items_1(self): - url = reverse('bot:offtopicchannelname-list', host='api') - response = self.client.get(f'{url}?random_items=1&mark_used=true') - - self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), [self.test_name.name]) - def test_running_out_of_names_with_mark_used_parameter(self): + def test_running_out_of_names_with_random_parameter(self): url = reverse('bot:offtopicchannelname-list', host='api') - response = self.client.get(f'{url}?random_items=2&mark_used=true') + response = self.client.get(f'{url}?random_items=2') self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), [self.test_name.name, self.test_name_2.name]) -- cgit v1.2.3 From 72e3ee0645d8017bfdac27f9fd876137164f3e3e Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 28 May 2020 09:30:05 +0300 Subject: OT Tests: Add docstring to tests --- pydis_site/apps/api/tests/test_off_topic_channel_names.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pydis_site/apps/api/tests/test_off_topic_channel_names.py b/pydis_site/apps/api/tests/test_off_topic_channel_names.py index 51372651..cac9405a 100644 --- a/pydis_site/apps/api/tests/test_off_topic_channel_names.py +++ b/pydis_site/apps/api/tests/test_off_topic_channel_names.py @@ -10,12 +10,14 @@ class UnauthenticatedTests(APISubdomainTestCase): self.client.force_authenticate(user=None) def test_cannot_read_off_topic_channel_name_list(self): + """Test does this return 401 response code when not authenticated.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(url) self.assertEqual(response.status_code, 401) def test_cannot_read_off_topic_channel_name_list_with_random_item_param(self): + """Test does this give 401 code when `random_items` provided and not authenticated.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=no') @@ -24,6 +26,7 @@ class UnauthenticatedTests(APISubdomainTestCase): class EmptyDatabaseTests(APISubdomainTestCase): def test_returns_empty_object(self): + """Test does this return empty list when no names in database.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(url) @@ -31,6 +34,7 @@ class EmptyDatabaseTests(APISubdomainTestCase): self.assertEqual(response.json(), []) def test_returns_empty_list_with_get_all_param(self): + """Test does this return empty list when no names and `random_items` param provided.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=5') @@ -38,6 +42,7 @@ class EmptyDatabaseTests(APISubdomainTestCase): self.assertEqual(response.json(), []) def test_returns_400_for_bad_random_items_param(self): + """Test does this return error message when passing not integer as `random_items`.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=totally-a-valid-integer') @@ -47,6 +52,7 @@ class EmptyDatabaseTests(APISubdomainTestCase): }) def test_returns_400_for_negative_random_items_param(self): + """Test does this return error message when passing negative int as `random_items`.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=-5') @@ -63,6 +69,7 @@ class ListTests(APISubdomainTestCase): cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk', used=True) def test_returns_name_in_list(self): + """Test does this return all off-topic channel names.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(url) @@ -76,6 +83,7 @@ class ListTests(APISubdomainTestCase): ) def test_returns_single_item_with_random_items_param_set_to_1(self): + """Test does this return not-used name instead used.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=1') @@ -84,6 +92,7 @@ class ListTests(APISubdomainTestCase): self.assertEqual(response.json(), [self.test_name.name]) def test_running_out_of_names_with_random_parameter(self): + """Test does this reset names `used` parameter to `False` when running out of names.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=2') @@ -101,6 +110,7 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response.status_code, 201) def test_returns_201_for_unicode_chars(self): + """Test does this accept all valid characters.""" url = reverse('bot:offtopicchannelname-list', host='api') names = ( '๐– ๐–ก๐–ข๐–ฃ๐–ค๐–ฅ๐–ฆ๐–ง๐–จ๐–ฉ๐–ช๐–ซ๐–ฌ๐–ญ๐–ฎ๐–ฏ๐–ฐ๐–ฑ๐–ฒ๐–ณ๐–ด๐–ต๐–ถ๐–ท๐–ธ๐–น', @@ -112,6 +122,7 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response.status_code, 201) def test_returns_400_for_missing_name_param(self): + """Test does this return error message when name not provided.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.post(url) self.assertEqual(response.status_code, 400) @@ -120,6 +131,7 @@ class CreationTests(APISubdomainTestCase): }) def test_returns_400_for_bad_name_param(self): + """Test does this return error message when invalid characters provided.""" url = reverse('bot:offtopicchannelname-list', host='api') invalid_names = ( 'space between words', @@ -142,18 +154,21 @@ class DeletionTests(APISubdomainTestCase): cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk') def test_deleting_unknown_name_returns_404(self): + """Test does this return 404 code when trying to delete unknown name.""" url = reverse('bot:offtopicchannelname-detail', args=('unknown-name',), host='api') response = self.client.delete(url) self.assertEqual(response.status_code, 404) def test_deleting_known_name_returns_204(self): + """Test does this return 204 code when deleting was successful.""" url = reverse('bot:offtopicchannelname-detail', args=(self.test_name.name,), host='api') response = self.client.delete(url) self.assertEqual(response.status_code, 204) def test_name_gets_deleted(self): + """Test does name gets actually deleted.""" url = reverse('bot:offtopicchannelname-detail', args=(self.test_name_2.name,), host='api') response = self.client.delete(url) -- cgit v1.2.3 From 5c53af373f9f868c5603a903a2c2d9636c2d0982 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 28 May 2020 09:35:17 +0300 Subject: OT: Fix comments --- pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py index e6cf8172..c4520a48 100644 --- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py @@ -113,16 +113,16 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): name__in=(query.name for query in queryset) ).update(used=True) - # When client request more channel names than non-used names is available, start - # new round of names. + # When the client requests more channel names than are available, + # we reset all names to used=False and start a new round of names. if len(queryset) < random_count: - # Get how much names still missing and don't fetch duplicate names. + # Figure out how many additional names we need, and don't fetch duplicate names. need_more = random_count - len(queryset) ext = self.get_queryset().order_by('?').exclude( name__in=(query.name for query in queryset) )[:need_more] - # Set all names `used` field to False except these that we just used. + # Reset the `used` field to False for all names except the ones we just used. self.get_queryset().exclude(name__in=( query.name for query in ext) ).update(used=False) -- cgit v1.2.3 From e046ac1d764d80a6a04c3c2d70fb8b4d718b6210 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 28 May 2020 09:36:24 +0300 Subject: OT: Rename variable `need_more` to `names_needed` --- pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py index c4520a48..1099922c 100644 --- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py @@ -117,10 +117,10 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): # we reset all names to used=False and start a new round of names. if len(queryset) < random_count: # Figure out how many additional names we need, and don't fetch duplicate names. - need_more = random_count - len(queryset) + names_needed = random_count - len(queryset) ext = self.get_queryset().order_by('?').exclude( name__in=(query.name for query in queryset) - )[:need_more] + )[:names_needed] # Reset the `used` field to False for all names except the ones we just used. self.get_queryset().exclude(name__in=( -- cgit v1.2.3 From 9cedba3870947ca10218c65c4106f5dd095d230c Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 28 May 2020 09:37:29 +0300 Subject: OT: Rename variable `ext` to `other_names` --- pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py index 1099922c..9af69ae4 100644 --- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py @@ -118,18 +118,18 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): if len(queryset) < random_count: # Figure out how many additional names we need, and don't fetch duplicate names. names_needed = random_count - len(queryset) - ext = self.get_queryset().order_by('?').exclude( + other_names = self.get_queryset().order_by('?').exclude( name__in=(query.name for query in queryset) )[:names_needed] # Reset the `used` field to False for all names except the ones we just used. self.get_queryset().exclude(name__in=( - query.name for query in ext) + query.name for query in other_names) ).update(used=False) # Join original queryset (that had missing names) # and extension with these missing names. - queryset = list(queryset) + list(ext) + queryset = list(queryset) + list(other_names) serialized = self.serializer_class(queryset, many=True) return Response(serialized.data) -- cgit v1.2.3 From 518a39b3e2b56f942328695d5e44674976317c7c Mon Sep 17 00:00:00 2001 From: Dennis Pham Date: Sat, 8 Aug 2020 14:46:04 -0400 Subject: Update rule 6 for the removal of #show-your-projects --- pydis_site/apps/api/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pydis_site/apps/api/views.py b/pydis_site/apps/api/views.py index a73d4718..7ac56641 100644 --- a/pydis_site/apps/api/views.py +++ b/pydis_site/apps/api/views.py @@ -140,7 +140,8 @@ class RulesView(APIView): ), ( "No spamming or unapproved advertising, including requests for paid work. " - "Open-source projects can be showcased in #show-your-projects." + "Open-source projects can be shared with others in #python-general and " + "code reviews can be asked for in a help channel." ), ( "Keep discussions relevant to channel topics and guidelines." -- cgit v1.2.3 From 4e67dbc2512653536de3eb603c537c8e40f058f6 Mon Sep 17 00:00:00 2001 From: Dennis Pham Date: Thu, 13 Aug 2020 18:22:14 -0400 Subject: Update Code Jam 7 to most recent in navbar --- pydis_site/templates/base/navbar.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/templates/base/navbar.html b/pydis_site/templates/base/navbar.html index f07662ae..c2915025 100644 --- a/pydis_site/templates/base/navbar.html +++ b/pydis_site/templates/base/navbar.html @@ -88,7 +88,7 @@ Events - Upcoming: Code Jam 7 + Most Recent: Code Jam 7 All events -- cgit v1.2.3 From 0ba6e25f88400e9a54ae9be2c176687ad69bb94f Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Thu, 20 Aug 2020 15:15:21 +0200 Subject: Allow direct fetching of reminders by id --- pydis_site/apps/api/tests/test_reminders.py | 28 ++++++++++++++++++++++++++++ pydis_site/apps/api/viewsets/bot/reminder.py | 8 +++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/pydis_site/apps/api/tests/test_reminders.py b/pydis_site/apps/api/tests/test_reminders.py index a05d9296..9dffb668 100644 --- a/pydis_site/apps/api/tests/test_reminders.py +++ b/pydis_site/apps/api/tests/test_reminders.py @@ -163,6 +163,34 @@ class ReminderListTests(APISubdomainTestCase): self.assertEqual(response.json(), [self.rem_dict_one]) +class ReminderRetrieveTests(APISubdomainTestCase): + @classmethod + def setUpTestData(cls): + cls.author = User.objects.create( + id=6789, + name='Reminder author', + discriminator=6789, + ) + + cls.reminder = Reminder.objects.create( + author=cls.author, + content="Reminder content", + expiration=datetime.utcnow().isoformat(), + jump_url="http://example.com/", + channel_id=123 + ) + + def test_retrieve_unknown_returns_404(self): + url = reverse('bot:reminder-detail', args=("not_an_id",), host='api') + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + + def test_retrieve_known_returns_200(self): + url = reverse('bot:reminder-detail', args=(self.reminder.id,), host='api') + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + class ReminderUpdateTests(APISubdomainTestCase): @classmethod def setUpTestData(cls): diff --git a/pydis_site/apps/api/viewsets/bot/reminder.py b/pydis_site/apps/api/viewsets/bot/reminder.py index 6f8a28f2..00413fb1 100644 --- a/pydis_site/apps/api/viewsets/bot/reminder.py +++ b/pydis_site/apps/api/viewsets/bot/reminder.py @@ -4,6 +4,7 @@ from rest_framework.mixins import ( CreateModelMixin, DestroyModelMixin, ListModelMixin, + RetrieveModelMixin, UpdateModelMixin ) from rest_framework.viewsets import GenericViewSet @@ -13,7 +14,12 @@ from pydis_site.apps.api.serializers import ReminderSerializer class ReminderViewSet( - CreateModelMixin, ListModelMixin, DestroyModelMixin, UpdateModelMixin, GenericViewSet + CreateModelMixin, + RetrieveModelMixin, + ListModelMixin, + DestroyModelMixin, + UpdateModelMixin, + GenericViewSet, ): """ View providing CRUD access to reminders. -- cgit v1.2.3 From 700de6b08b70f40dc97f3644d4e0ee32ab8f6ccf Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Thu, 20 Aug 2020 15:24:29 +0200 Subject: Update docstring for new fetching behaviour --- pydis_site/apps/api/viewsets/bot/reminder.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/pydis_site/apps/api/viewsets/bot/reminder.py b/pydis_site/apps/api/viewsets/bot/reminder.py index 00413fb1..111660d9 100644 --- a/pydis_site/apps/api/viewsets/bot/reminder.py +++ b/pydis_site/apps/api/viewsets/bot/reminder.py @@ -50,6 +50,30 @@ class ReminderViewSet( #### Status codes - 200: returned on success + ### GET /bot/reminders/ + Fetches the reminder with the given id. + + #### Response format + >>> + ... { + ... 'active': True, + ... 'author': 1020103901030, + ... 'mentions': [ + ... 336843820513755157, + ... 165023948638126080, + ... 267628507062992896 + ... ], + ... 'content': "Make dinner", + ... 'expiration': '5018-11-20T15:52:00Z', + ... 'id': 11, + ... 'channel_id': 634547009956872193, + ... 'jump_url': "https://discord.com/channels///" + ... } + + #### Status codes + - 200: returned on success + - 404: returned when the reminder doesn't exist + ### POST /bot/reminders Create a new reminder. -- cgit v1.2.3 From 9e70bb65627f0b934aa1d22195a18778fba2d592 Mon Sep 17 00:00:00 2001 From: Leon Sandรธy Date: Sat, 22 Aug 2020 01:35:36 +0200 Subject: Add gitpython ~=3.1.7 as a dependency. We'll need this to fetch the hash. --- Pipfile | 1 + Pipfile.lock | 206 +++++++++++++++++++++++++++++++++-------------------------- 2 files changed, 117 insertions(+), 90 deletions(-) diff --git a/Pipfile b/Pipfile index 0f794078..d23d28e6 100644 --- a/Pipfile +++ b/Pipfile @@ -20,6 +20,7 @@ pyyaml = "~=5.1" pyuwsgi = {version = "~=2.0", sys_platform = "!='win32'"} django-allauth = "~=0.41" sentry-sdk = "~=0.14" +gitpython = "~=3.1.7" [dev-packages] coverage = "~=5.0" diff --git a/Pipfile.lock b/Pipfile.lock index 02d81d76..b8c85d33 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "edc59f1c711954bd22606d68e00f44c21c68a7b3193b20e44a86438e24c0f54b" + "sha256": "ad586b840e82b4ae87eed1af70adde0b2c8b7f862a832c4cfa87748b97add3bd" }, "pipfile-spec": 6, "requires": { @@ -56,11 +56,11 @@ }, "django": { "hashes": [ - "sha256:31a5fbbea5fc71c99e288ec0b2f00302a0a92c44b13ede80b73a6a4d6d205582", - "sha256:5457fc953ec560c5521b41fad9e6734a4668b7ba205832191bbdff40ec61073c" + "sha256:96fbe04e8ba0df289171e7f6970e0ff8b472bf4f909ed9e0e5beccbac7e1dbbe", + "sha256:c22b4cd8e388f8219dc121f091e53a8701f9f5bca9aa132b5254263cab516215" ], "index": "pypi", - "version": "==3.0.8" + "version": "==3.0.9" }, "django-allauth": { "hashes": [ @@ -129,19 +129,19 @@ }, "django-simple-bulma": { "hashes": [ - "sha256:a1462088791af5c65d2ea3b5a517a481dd8afb35b324979cdeefa6f3e6c58d3d", - "sha256:a93daf425353834db96840ca4aa7744c796899243f114e73b8159724ce4573c1" + "sha256:79928fa983151947c635acf65fa5177ca775db98c8d53ddf1c785fe48c727466", + "sha256:e5cff3fc5f0d45558362ab8d0e11f92887c4fc85616f77daa6174940f94b12c7" ], "index": "pypi", - "version": "==1.2.0" + "version": "==1.3.2" }, "djangorestframework": { "hashes": [ - "sha256:05809fc66e1c997fd9a32ea5730d9f4ba28b109b9da71fccfa5ff241201fd0a4", - "sha256:e782087823c47a26826ee5b6fa0c542968219263fb3976ec3c31edab23a4001f" + "sha256:6dd02d5a4bd2516fb93f80360673bf540c3b6641fec8766b1da2870a5aa00b32", + "sha256:8b1ac62c581dbc5799b03e535854b92fc4053ecfe74bad3f9c05782063d4196b" ], "index": "pypi", - "version": "==3.11.0" + "version": "==3.11.1" }, "djangorestframework-bulk": { "hashes": [ @@ -150,6 +150,22 @@ "index": "pypi", "version": "==0.2.1" }, + "gitdb": { + "hashes": [ + "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac", + "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9" + ], + "markers": "python_version >= '3.4'", + "version": "==4.0.5" + }, + "gitpython": { + "hashes": [ + "sha256:2db287d71a284e22e5c2846042d0602465c7434d910406990d5b74df4afb0858", + "sha256:fa3b92da728a457dd75d62bb5f3eb2816d99a7fe6c67398e260637a40e3fafb5" + ], + "index": "pypi", + "version": "==3.1.7" + }, "idna": { "hashes": [ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", @@ -210,32 +226,34 @@ }, "pillow": { "hashes": [ - "sha256:0295442429645fa16d05bd567ef5cff178482439c9aad0411d3f0ce9b88b3a6f", - "sha256:06aba4169e78c439d528fdeb34762c3b61a70813527a2c57f0540541e9f433a8", - "sha256:09d7f9e64289cb40c2c8d7ad674b2ed6105f55dc3b09aa8e4918e20a0311e7ad", - "sha256:0a80dd307a5d8440b0a08bd7b81617e04d870e40a3e46a32d9c246e54705e86f", - "sha256:1ca594126d3c4def54babee699c055a913efb01e106c309fa6b04405d474d5ae", + "sha256:c92302a33138409e8f1ad16731568c55c9053eee71bb05b6b744067e1b62380f", "sha256:25930fadde8019f374400f7986e8404c8b781ce519da27792cbe46eabec00c4d", - "sha256:431b15cffbf949e89df2f7b48528be18b78bfa5177cb3036284a5508159492b5", + "sha256:1ca594126d3c4def54babee699c055a913efb01e106c309fa6b04405d474d5ae", "sha256:52125833b070791fcb5710fabc640fc1df07d087fc0c0f02d3661f76c23c5b8b", - "sha256:5e51ee2b8114def244384eda1c82b10e307ad9778dac5c83fb0943775a653cd8", - "sha256:612cfda94e9c8346f239bf1a4b082fdd5c8143cf82d685ba2dba76e7adeeb233", "sha256:6d7741e65835716ceea0fd13a7d0192961212fd59e741a46bbed7a473c634ed6", - "sha256:6edb5446f44d901e8683ffb25ebdfc26988ee813da3bf91e12252b57ac163727", - "sha256:725aa6cfc66ce2857d585f06e9519a1cc0ef6d13f186ff3447ab6dff0a09bc7f", - "sha256:8dad18b69f710bf3a001d2bf3afab7c432785d94fcf819c16b5207b1cfd17d38", + "sha256:9c87ef410a58dd54b92424ffd7e28fd2ec65d2f7fc02b76f5e9b2067e355ebf6", "sha256:94cf49723928eb6070a892cb39d6c156f7b5a2db4e8971cb958f7b6b104fb4c4", - "sha256:97f9e7953a77d5a70f49b9a48da7776dc51e9b738151b22dacf101641594a626", - "sha256:9ad7f865eebde135d526bb3163d0b23ffff365cf87e767c649550964ad72785d", + "sha256:edf31f1150778abd4322444c393ab9c7bd2af271dd4dafb4208fb613b1f3cdc9", + "sha256:612cfda94e9c8346f239bf1a4b082fdd5c8143cf82d685ba2dba76e7adeeb233", "sha256:a060cf8aa332052df2158e5a119303965be92c3da6f2d93b6878f0ebca80b2f6", + "sha256:ffe538682dc19cc542ae7c3e504fdf54ca7f86fb8a135e59dd6bc8627eae6cce", + "sha256:8dad18b69f710bf3a001d2bf3afab7c432785d94fcf819c16b5207b1cfd17d38", "sha256:c79f9c5fb846285f943aafeafda3358992d64f0ef58566e23484132ecd8d7d63", - "sha256:c92302a33138409e8f1ad16731568c55c9053eee71bb05b6b744067e1b62380f", - "sha256:d08b23fdb388c0715990cbc06866db554e1822c4bdcf6d4166cf30ac82df8c41", - "sha256:d350f0f2c2421e65fbc62690f26b59b0bcda1b614beb318c81e38647e0f673a1", "sha256:ec29604081f10f16a7aea809ad42e27764188fc258b02259a03a8ff7ded3808d", - "sha256:edf31f1150778abd4322444c393ab9c7bd2af271dd4dafb4208fb613b1f3cdc9", + "sha256:725aa6cfc66ce2857d585f06e9519a1cc0ef6d13f186ff3447ab6dff0a09bc7f", + "sha256:06aba4169e78c439d528fdeb34762c3b61a70813527a2c57f0540541e9f433a8", + "sha256:9ad7f865eebde135d526bb3163d0b23ffff365cf87e767c649550964ad72785d", + "sha256:09d7f9e64289cb40c2c8d7ad674b2ed6105f55dc3b09aa8e4918e20a0311e7ad", + "sha256:0a80dd307a5d8440b0a08bd7b81617e04d870e40a3e46a32d9c246e54705e86f", + "sha256:97f9e7953a77d5a70f49b9a48da7776dc51e9b738151b22dacf101641594a626", "sha256:f7e30c27477dffc3e85c2463b3e649f751789e0f6c8456099eea7ddd53be4a8a", - "sha256:ffe538682dc19cc542ae7c3e504fdf54ca7f86fb8a135e59dd6bc8627eae6cce" + "sha256:6edb5446f44d901e8683ffb25ebdfc26988ee813da3bf91e12252b57ac163727", + "sha256:5e51ee2b8114def244384eda1c82b10e307ad9778dac5c83fb0943775a653cd8", + "sha256:0295442429645fa16d05bd567ef5cff178482439c9aad0411d3f0ce9b88b3a6f", + "sha256:431b15cffbf949e89df2f7b48528be18b78bfa5177cb3036284a5508159492b5", + "sha256:d08b23fdb388c0715990cbc06866db554e1822c4bdcf6d4166cf30ac82df8c41", + "sha256:d350f0f2c2421e65fbc62690f26b59b0bcda1b614beb318c81e38647e0f673a1", + "sha256:e901964262a56d9ea3c2693df68bc9860b8bdda2b04768821e4c44ae797de117" ], "markers": "python_version >= '3.5'", "version": "==7.2.0" @@ -360,11 +378,11 @@ }, "sentry-sdk": { "hashes": [ - "sha256:2f023ff348359ec5f0b73a840e8b08e6a8d3b2613a98c57d11c222ef43879237", - "sha256:380a280cfc7c4ade5912294e6d9aa71ce776b5fca60a3782e9331b0bcd2866bf" + "sha256:d359609e23ec9360b61e5ffdfa417e2f6bca281bfb869608c98c169c7e64acd5", + "sha256:e12eb1c2c01cd9e9cfe70608dbda4ef451f37ef0b7cbb92e5d43f87c341d6334" ], "index": "pypi", - "version": "==0.16.1" + "version": "==0.16.5" }, "six": { "hashes": [ @@ -374,6 +392,14 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, + "smmap": { + "hashes": [ + "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4", + "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==3.0.4" + }, "sorl-thumbnail": { "hashes": [ "sha256:66771521f3c0ed771e1ce8e1aaf1639ebff18f7f5a40cfd3083da8f0fe6c7c99", @@ -392,11 +418,11 @@ }, "urllib3": { "hashes": [ - "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", - "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" + "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", + "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.25.9" + "version": "==1.25.10" }, "webencodings": { "hashes": [ @@ -407,11 +433,11 @@ }, "whitenoise": { "hashes": [ - "sha256:60154b976a13901414a25b0273a841145f77eb34a141f9ae032a0ace3e4d5b27", - "sha256:6dd26bfda3af29177d8ab7333a0c7b7642eb615ce83764f4d15a9aecda3201c4" + "sha256:05ce0be39ad85740a78750c86a93485c40f08ad8c62a6006de0233765996e5c7", + "sha256:05d00198c777028d72d8b0bbd234db605ef6d60e9410125124002518a48e515d" ], "index": "pypi", - "version": "==5.1.0" + "version": "==5.2.0" }, "wiki": { "hashes": [ @@ -440,11 +466,11 @@ }, "attrs": { "hashes": [ - "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", - "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a", + "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==19.3.0" + "version": "==20.1.0" }, "bandit": { "hashes": [ @@ -455,51 +481,51 @@ }, "cfgv": { "hashes": [ - "sha256:1ccf53320421aeeb915275a196e23b3b8ae87dea8ac6698b1638001d4a486d53", - "sha256:c8e8f552ffcc6194f4e18dd4f68d9aef0c0d58ae7e7be8c82bee3c5e9edfa513" + "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d", + "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1" ], "markers": "python_full_version >= '3.6.1'", - "version": "==3.1.0" + "version": "==3.2.0" }, "coverage": { "hashes": [ - "sha256:0fc4e0d91350d6f43ef6a61f64a48e917637e1dcfcba4b4b7d543c628ef82c2d", - "sha256:10f2a618a6e75adf64329f828a6a5b40244c1c50f5ef4ce4109e904e69c71bd2", - "sha256:12eaccd86d9a373aea59869bc9cfa0ab6ba8b1477752110cb4c10d165474f703", - "sha256:1874bdc943654ba46d28f179c1846f5710eda3aeb265ff029e0ac2b52daae404", - "sha256:1dcebae667b73fd4aa69237e6afb39abc2f27520f2358590c1b13dd90e32abe7", - "sha256:1e58fca3d9ec1a423f1b7f2aa34af4f733cbfa9020c8fe39ca451b6071237405", - "sha256:214eb2110217f2636a9329bc766507ab71a3a06a8ea30cdeebb47c24dce5972d", - "sha256:25fe74b5b2f1b4abb11e103bb7984daca8f8292683957d0738cd692f6a7cc64c", - "sha256:32ecee61a43be509b91a526819717d5e5650e009a8d5eda8631a59c721d5f3b6", - "sha256:3740b796015b889e46c260ff18b84683fa2e30f0f75a171fb10d2bf9fb91fc70", - "sha256:3b2c34690f613525672697910894b60d15800ac7e779fbd0fccf532486c1ba40", - "sha256:41d88736c42f4a22c494c32cc48a05828236e37c991bd9760f8923415e3169e4", - "sha256:42fa45a29f1059eda4d3c7b509589cc0343cd6bbf083d6118216830cd1a51613", - "sha256:4bb385a747e6ae8a65290b3df60d6c8a692a5599dc66c9fa3520e667886f2e10", - "sha256:509294f3e76d3f26b35083973fbc952e01e1727656d979b11182f273f08aa80b", - "sha256:5c74c5b6045969b07c9fb36b665c9cac84d6c174a809fc1b21bdc06c7836d9a0", - "sha256:60a3d36297b65c7f78329b80120f72947140f45b5c7a017ea730f9112b40f2ec", - "sha256:6f91b4492c5cde83bfe462f5b2b997cdf96a138f7c58b1140f05de5751623cf1", - "sha256:7403675df5e27745571aba1c957c7da2dacb537c21e14007ec3a417bf31f7f3d", - "sha256:87bdc8135b8ee739840eee19b184804e5d57f518578ffc797f5afa2c3c297913", - "sha256:8a3decd12e7934d0254939e2bf434bf04a5890c5bf91a982685021786a08087e", - "sha256:9702e2cb1c6dec01fb8e1a64c015817c0800a6eca287552c47a5ee0ebddccf62", - "sha256:a4d511012beb967a39580ba7d2549edf1e6865a33e5fe51e4dce550522b3ac0e", - "sha256:bbb387811f7a18bdc61a2ea3d102be0c7e239b0db9c83be7bfa50f095db5b92a", - "sha256:bfcc811883699ed49afc58b1ed9f80428a18eb9166422bce3c31a53dba00fd1d", - "sha256:c32aa13cc3fe86b0f744dfe35a7f879ee33ac0a560684fef0f3e1580352b818f", - "sha256:ca63dae130a2e788f2b249200f01d7fa240f24da0596501d387a50e57aa7075e", - "sha256:d54d7ea74cc00482a2410d63bf10aa34ebe1c49ac50779652106c867f9986d6b", - "sha256:d67599521dff98ec8c34cd9652cbcfe16ed076a2209625fca9dc7419b6370e5c", - "sha256:d82db1b9a92cb5c67661ca6616bdca6ff931deceebb98eecbd328812dab52032", - "sha256:d9ad0a988ae20face62520785ec3595a5e64f35a21762a57d115dae0b8fb894a", - "sha256:ebf2431b2d457ae5217f3a1179533c456f3272ded16f8ed0b32961a6d90e38ee", - "sha256:ed9a21502e9223f563e071759f769c3d6a2e1ba5328c31e86830368e8d78bc9c", - "sha256:f50632ef2d749f541ca8e6c07c9928a37f87505ce3a9f20c8446ad310f1aa87b" - ], - "index": "pypi", - "version": "==5.2" + "sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb", + "sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3", + "sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716", + "sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034", + "sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3", + "sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8", + "sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0", + "sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f", + "sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4", + "sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962", + "sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d", + "sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b", + "sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4", + "sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3", + "sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258", + "sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59", + "sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01", + "sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd", + "sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b", + "sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d", + "sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89", + "sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd", + "sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b", + "sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d", + "sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46", + "sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546", + "sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082", + "sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b", + "sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4", + "sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8", + "sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811", + "sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd", + "sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651", + "sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0" + ], + "index": "pypi", + "version": "==5.2.1" }, "distlib": { "hashes": [ @@ -605,16 +631,16 @@ "sha256:2db287d71a284e22e5c2846042d0602465c7434d910406990d5b74df4afb0858", "sha256:fa3b92da728a457dd75d62bb5f3eb2816d99a7fe6c67398e260637a40e3fafb5" ], - "markers": "python_version >= '3.4'", + "index": "pypi", "version": "==3.1.7" }, "identify": { "hashes": [ - "sha256:06b4373546ae55eaaefdac54f006951dbd968fe2912846c00e565b09cfaed101", - "sha256:5519601b70c831011fb425ffd214101df7639ba3980f24dc283f7675b19127b3" + "sha256:69c4769f085badafd0e04b1763e847258cbbf6d898e8678ebffc91abdb86f6c6", + "sha256:d6ae6daee50ba1b493e9ca4d36a5edd55905d2cf43548fdc20b2a14edef102e7" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.4.24" + "version": "==1.4.28" }, "importlib-metadata": { "hashes": [ @@ -769,19 +795,19 @@ }, "unittest-xml-reporting": { "hashes": [ - "sha256:74eaf7739a7957a74f52b8187c5616f61157372189bef0a32ba5c30bbc00e58a", - "sha256:e09b8ae70cce9904cdd331f53bf929150962869a5324ab7ff3dd6c8b87e01f7d" + "sha256:7bf515ea8cb244255a25100cd29db611a73f8d3d0aaf672ed3266307e14cc1ca", + "sha256:984cebba69e889401bfe3adb9088ca376b3a1f923f0590d005126c1bffd1a695" ], "index": "pypi", - "version": "==3.0.2" + "version": "==3.0.4" }, "virtualenv": { "hashes": [ - "sha256:26cdd725a57fef4c7c22060dba4647ebd8ca377e30d1c1cf547b30a0b79c43b4", - "sha256:c51f1ba727d1614ce8fd62457748b469fbedfdab2c7e5dd480c9ae3fbe1233f1" + "sha256:43add625c53c596d38f971a465553f6318decc39d98512bc100fa1b1e839c8dc", + "sha256:e0305af10299a7fb0d69393d8f04cb2965dda9351140d11ac8db4e5e3970451b" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.0.27" + "version": "==20.0.31" }, "zipp": { "hashes": [ -- cgit v1.2.3 From 1d4db565aedb1b13b8594f4586748698d85d6139 Mon Sep 17 00:00:00 2001 From: Leon Sandรธy Date: Sat, 22 Aug 2020 01:36:05 +0200 Subject: Install git in the docker environment. This is required for gitpython to work. --- docker/Dockerfile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docker/Dockerfile b/docker/Dockerfile index aa427947..d9da7d14 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -8,6 +8,11 @@ ENV PIP_NO_CACHE_DIR=false \ PIPENV_HIDE_EMOJIS=1 \ PIPENV_NOSPIN=1 +# Install git +RUN apt-get update && \ + apt-get upgrade -y && \ + apt-get install -y git + # Create non-root user. RUN useradd --system --shell /bin/false --uid 1500 pysite -- cgit v1.2.3 From 1a55e92824d56a34a677c1f764a5393038e66955 Mon Sep 17 00:00:00 2001 From: Leon Sandรธy Date: Sat, 22 Aug 2020 01:40:55 +0200 Subject: Add a context processor that outputs the git SHA. This will make the SHA available in all templates. --- pydis_site/context_processors.py | 10 ++++++++++ pydis_site/settings.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 pydis_site/context_processors.py diff --git a/pydis_site/context_processors.py b/pydis_site/context_processors.py new file mode 100644 index 00000000..4ae0dbb3 --- /dev/null +++ b/pydis_site/context_processors.py @@ -0,0 +1,10 @@ +import git +from django.template import RequestContext + +REPO = git.Repo(search_parent_directories=True) +SHA = REPO.head.object.hexsha + + +def git_sha_processor(_: RequestContext) -> dict: + """Expose the git SHA for this repo to all views.""" + return {'git_sha': SHA} diff --git a/pydis_site/settings.py b/pydis_site/settings.py index 2c87007c..afa097be 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -157,8 +157,8 @@ TEMPLATES = [ 'django.template.context_processors.static', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', - "sekizai.context_processors.sekizai", + "pydis_site.context_processors.git_sha_processor" ], }, }, -- cgit v1.2.3 From 75181a55977abe2eb6f3ac782b51bf13b8945024 Mon Sep 17 00:00:00 2001 From: Leon Sandรธy Date: Sat, 22 Aug 2020 01:41:20 +0200 Subject: Add a comment with the git SHA to base templates. --- pydis_site/templates/base/base.html | 1 + pydis_site/templates/wiki/base.html | 1 + 2 files changed, 2 insertions(+) diff --git a/pydis_site/templates/base/base.html b/pydis_site/templates/base/base.html index 4c70d778..70426dc1 100644 --- a/pydis_site/templates/base/base.html +++ b/pydis_site/templates/base/base.html @@ -37,6 +37,7 @@ {% render_block "css" %} +
{% if messages %} diff --git a/pydis_site/templates/wiki/base.html b/pydis_site/templates/wiki/base.html index 846492ab..c9faf914 100644 --- a/pydis_site/templates/wiki/base.html +++ b/pydis_site/templates/wiki/base.html @@ -19,6 +19,7 @@ {% endblock %} {% block content %} + {% block site_navbar %} {% include "base/navbar.html" %} {% endblock %} -- cgit v1.2.3 From 09538085645d623b8cb685c602b3a4e524b6408f Mon Sep 17 00:00:00 2001 From: Leon Sandรธy Date: Sat, 22 Aug 2020 02:03:19 +0200 Subject: Optimize the git install in Dockerfile. --- docker/Dockerfile | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index d9da7d14..d3d0319d 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -9,9 +9,13 @@ ENV PIP_NO_CACHE_DIR=false \ PIPENV_NOSPIN=1 # Install git -RUN apt-get update && \ - apt-get upgrade -y && \ - apt-get install -y git +RUN apt-get update +RUN apt-get install -y git + +RUN apt-get -y update \ + && apt-get install -y \ + git \ + && rm -rf /var/lib/apt/lists/* # Create non-root user. RUN useradd --system --shell /bin/false --uid 1500 pysite -- cgit v1.2.3 From 60bc529e4b1cfa59e82ede102b88fca08f8f93b4 Mon Sep 17 00:00:00 2001 From: Leon Sandรธy Date: Sat, 22 Aug 2020 02:04:44 +0200 Subject: Move git SHA initialization to __init__.py. This will make it easier to use in multiple places. --- pydis_site/__init__.py | 5 +++++ pydis_site/context_processors.py | 6 ++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pydis_site/__init__.py b/pydis_site/__init__.py index df67cf71..f0702577 100644 --- a/pydis_site/__init__.py +++ b/pydis_site/__init__.py @@ -1,3 +1,4 @@ +import git from wiki.plugins.macros.mdx import toc # Remove the toc header prefix. There's no option for this, so we gotta monkey patch it. @@ -7,3 +8,7 @@ toc.HEADER_ID_PREFIX = '' # by a string because Allauth won't let us just give it a list _there_, we have to point # at a list _somewhere else_ instead. VALIDATORS = [] + +# Git SHA +repo = git.Repo(search_parent_directories=True) +GIT_SHA = repo.head.object.hexsha diff --git a/pydis_site/context_processors.py b/pydis_site/context_processors.py index 4ae0dbb3..bb66f21d 100644 --- a/pydis_site/context_processors.py +++ b/pydis_site/context_processors.py @@ -1,10 +1,8 @@ -import git from django.template import RequestContext -REPO = git.Repo(search_parent_directories=True) -SHA = REPO.head.object.hexsha +from pydis_site import GIT_SHA def git_sha_processor(_: RequestContext) -> dict: """Expose the git SHA for this repo to all views.""" - return {'git_sha': SHA} + return {'git_sha': GIT_SHA} -- cgit v1.2.3 From 212a06f400bfce95281e4c8d11baf6cb09e70861 Mon Sep 17 00:00:00 2001 From: Leon Sandรธy Date: Sat, 22 Aug 2020 02:05:24 +0200 Subject: Set the sentry_sdk.init release to git_sha. This is the first step in implementing releases for Sentry. --- pydis_site/settings.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pydis_site/settings.py b/pydis_site/settings.py index afa097be..ff2942f0 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -20,6 +20,7 @@ import sentry_sdk from django.contrib.messages import constants as messages from sentry_sdk.integrations.django import DjangoIntegration +from pydis_site import GIT_SHA if typing.TYPE_CHECKING: from django.contrib.auth.models import User @@ -33,7 +34,8 @@ env = environ.Env( sentry_sdk.init( dsn=env('SITE_SENTRY_DSN'), integrations=[DjangoIntegration()], - send_default_pii=True + send_default_pii=True, + release=f"pydis-site@{GIT_SHA}" ) # Build paths inside the project like this: os.path.join(BASE_DIR, ...) -- cgit v1.2.3 From 34ad66914d14043b653f3e2245a06a5613b6e975 Mon Sep 17 00:00:00 2001 From: Leon Sandรธy Date: Sat, 22 Aug 2020 02:17:49 +0200 Subject: Create a GitHub Action to make a Sentry release. This should be all we need to enable the releases feature in Sentry for this project. --- .github/workflows/sentry-release.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/sentry-release.yml diff --git a/.github/workflows/sentry-release.yml b/.github/workflows/sentry-release.yml new file mode 100644 index 00000000..87c85277 --- /dev/null +++ b/.github/workflows/sentry-release.yml @@ -0,0 +1,23 @@ +name: Create Sentry release + +on: + push: + branches: + - master + +jobs: + createSentryRelease: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@master + - name: Create a Sentry.io release + uses: tclindner/sentry-releases-action@v1.2.0 + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: python-discord + SENTRY_PROJECT: site + with: + tagName: ${{ github.sha }} + environment: production + releaseNamePrefix: pydis-site@ -- cgit v1.2.3 From 8ee3d0be942b081df22e7d72244edee46d2bb2c1 Mon Sep 17 00:00:00 2001 From: Leon Sandรธy Date: Sat, 22 Aug 2020 02:22:13 +0200 Subject: Oops, remove double git install from Dockerfile. --- docker/Dockerfile | 3 --- 1 file changed, 3 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index d3d0319d..97cb73d5 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -9,9 +9,6 @@ ENV PIP_NO_CACHE_DIR=false \ PIPENV_NOSPIN=1 # Install git -RUN apt-get update -RUN apt-get install -y git - RUN apt-get -y update \ && apt-get install -y \ git \ -- cgit v1.2.3 From 80bdfa7d25fef6b565371409ba81a4e3abae0d61 Mon Sep 17 00:00:00 2001 From: Leon Sandรธy Date: Sat, 22 Aug 2020 02:38:30 +0200 Subject: We need .git in the docker container for the SHA. --- .dockerignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.dockerignore b/.dockerignore index 236295ca..61fa291a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,6 @@ .cache .coverage .coveragerc -.git .github .gitignore .gitlab -- cgit v1.2.3 From 416a0874187fdf96346e3f504d6542b3214957db Mon Sep 17 00:00:00 2001 From: Leon Sandรธy Date: Sat, 22 Aug 2020 02:49:28 +0200 Subject: Move git SHA fetcher into utils. --- pydis_site/__init__.py | 5 ----- pydis_site/context_processors.py | 4 ++-- pydis_site/settings.py | 4 ++-- pydis_site/utils/resources.py | 10 ++++++++++ 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/pydis_site/__init__.py b/pydis_site/__init__.py index f0702577..df67cf71 100644 --- a/pydis_site/__init__.py +++ b/pydis_site/__init__.py @@ -1,4 +1,3 @@ -import git from wiki.plugins.macros.mdx import toc # Remove the toc header prefix. There's no option for this, so we gotta monkey patch it. @@ -8,7 +7,3 @@ toc.HEADER_ID_PREFIX = '' # by a string because Allauth won't let us just give it a list _there_, we have to point # at a list _somewhere else_ instead. VALIDATORS = [] - -# Git SHA -repo = git.Repo(search_parent_directories=True) -GIT_SHA = repo.head.object.hexsha diff --git a/pydis_site/context_processors.py b/pydis_site/context_processors.py index bb66f21d..e905d9c7 100644 --- a/pydis_site/context_processors.py +++ b/pydis_site/context_processors.py @@ -1,8 +1,8 @@ from django.template import RequestContext -from pydis_site import GIT_SHA +from pydis_site.utils.resources import get_git_sha def git_sha_processor(_: RequestContext) -> dict: """Expose the git SHA for this repo to all views.""" - return {'git_sha': GIT_SHA} + return {'git_sha': get_git_sha()} diff --git a/pydis_site/settings.py b/pydis_site/settings.py index ff2942f0..e707a526 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -20,7 +20,7 @@ import sentry_sdk from django.contrib.messages import constants as messages from sentry_sdk.integrations.django import DjangoIntegration -from pydis_site import GIT_SHA +from pydis_site.utils.resources import get_git_sha if typing.TYPE_CHECKING: from django.contrib.auth.models import User @@ -35,7 +35,7 @@ sentry_sdk.init( dsn=env('SITE_SENTRY_DSN'), integrations=[DjangoIntegration()], send_default_pii=True, - release=f"pydis-site@{GIT_SHA}" + release=f"pydis-site@{get_git_sha()}" ) # Build paths inside the project like this: os.path.join(BASE_DIR, ...) diff --git a/pydis_site/utils/resources.py b/pydis_site/utils/resources.py index 637fd785..d36c4b77 100644 --- a/pydis_site/utils/resources.py +++ b/pydis_site/utils/resources.py @@ -4,8 +4,13 @@ import glob import typing from dataclasses import dataclass +import git import yaml +# Git SHA +repo = git.Repo(search_parent_directories=True) +GIT_SHA = repo.head.object.hexsha + @dataclass class URL: @@ -89,3 +94,8 @@ def load_categories(order: typing.List[str]) -> typing.List[Category]: categories.append(Category.construct_from_directory(direc)) return categories + + +def get_git_sha() -> str: + """Get the Git SHA for this repo.""" + return GIT_SHA -- cgit v1.2.3 From 503bfba6855d3019a2739bb1adce69c457a8b26e Mon Sep 17 00:00:00 2001 From: Leon Sandรธy Date: Sat, 22 Aug 2020 03:18:25 +0200 Subject: Move git SHA fetcher into its own file. Fix tests. --- pydis_site/context_processors.py | 2 +- pydis_site/settings.py | 2 +- pydis_site/tests/test_utils.py | 11 +++++++++++ pydis_site/utils/__init__.py | 3 +++ pydis_site/utils/resources.py | 10 ---------- pydis_site/utils/utils.py | 10 ++++++++++ 6 files changed, 26 insertions(+), 12 deletions(-) create mode 100644 pydis_site/tests/test_utils.py create mode 100644 pydis_site/utils/__init__.py create mode 100644 pydis_site/utils/utils.py diff --git a/pydis_site/context_processors.py b/pydis_site/context_processors.py index e905d9c7..ab5a4168 100644 --- a/pydis_site/context_processors.py +++ b/pydis_site/context_processors.py @@ -1,6 +1,6 @@ from django.template import RequestContext -from pydis_site.utils.resources import get_git_sha +from pydis_site.utils import get_git_sha def git_sha_processor(_: RequestContext) -> dict: diff --git a/pydis_site/settings.py b/pydis_site/settings.py index e707a526..0a5b0eed 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -20,7 +20,7 @@ import sentry_sdk from django.contrib.messages import constants as messages from sentry_sdk.integrations.django import DjangoIntegration -from pydis_site.utils.resources import get_git_sha +from pydis_site.utils import get_git_sha if typing.TYPE_CHECKING: from django.contrib.auth.models import User diff --git a/pydis_site/tests/test_utils.py b/pydis_site/tests/test_utils.py new file mode 100644 index 00000000..f1419860 --- /dev/null +++ b/pydis_site/tests/test_utils.py @@ -0,0 +1,11 @@ +from django.test import TestCase + +from pydis_site.utils import get_git_sha +from pydis_site.utils.utils import GIT_SHA + + +class UtilsTests(TestCase): + + def test_git_sha(self): + """Test that the get_git_sha returns the correct SHA.""" + self.assertEqual(get_git_sha(), GIT_SHA) diff --git a/pydis_site/utils/__init__.py b/pydis_site/utils/__init__.py new file mode 100644 index 00000000..bb91b3d8 --- /dev/null +++ b/pydis_site/utils/__init__.py @@ -0,0 +1,3 @@ +from .utils import get_git_sha + +__all__ = ['get_git_sha'] diff --git a/pydis_site/utils/resources.py b/pydis_site/utils/resources.py index d36c4b77..637fd785 100644 --- a/pydis_site/utils/resources.py +++ b/pydis_site/utils/resources.py @@ -4,13 +4,8 @@ import glob import typing from dataclasses import dataclass -import git import yaml -# Git SHA -repo = git.Repo(search_parent_directories=True) -GIT_SHA = repo.head.object.hexsha - @dataclass class URL: @@ -94,8 +89,3 @@ def load_categories(order: typing.List[str]) -> typing.List[Category]: categories.append(Category.construct_from_directory(direc)) return categories - - -def get_git_sha() -> str: - """Get the Git SHA for this repo.""" - return GIT_SHA diff --git a/pydis_site/utils/utils.py b/pydis_site/utils/utils.py new file mode 100644 index 00000000..2033ea19 --- /dev/null +++ b/pydis_site/utils/utils.py @@ -0,0 +1,10 @@ +import git + +# Git SHA +repo = git.Repo(search_parent_directories=True) +GIT_SHA = repo.head.object.hexsha + + +def get_git_sha() -> str: + """Get the Git SHA for this repo.""" + return GIT_SHA -- cgit v1.2.3 From 52d7ae050d32df7ceda6888f8a60590ceb0e37dc Mon Sep 17 00:00:00 2001 From: Leon Sandรธy Date: Sat, 22 Aug 2020 03:23:07 +0200 Subject: omit resources.py from coverage reports. I'm not writing the tests for that, it has nothing to do with this branch. --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index a49af74e..f5ddf08d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -14,6 +14,7 @@ omit = */urls.py pydis_site/wsgi.py pydis_site/settings.py + pydis_site/utils/resources.py [report] fail_under = 100 -- cgit v1.2.3 From 88d364bfc809d20c6c4645abba75b889a526c804 Mon Sep 17 00:00:00 2001 From: Leon Sandรธy Date: Sat, 22 Aug 2020 03:46:08 +0200 Subject: Remove the SHA from the wiki base.html This extends the regular base.html, so this would cause wiki pages to have two SHA's. --- pydis_site/templates/wiki/base.html | 1 - 1 file changed, 1 deletion(-) diff --git a/pydis_site/templates/wiki/base.html b/pydis_site/templates/wiki/base.html index c9faf914..846492ab 100644 --- a/pydis_site/templates/wiki/base.html +++ b/pydis_site/templates/wiki/base.html @@ -19,7 +19,6 @@ {% endblock %} {% block content %} - {% block site_navbar %} {% include "base/navbar.html" %} {% endblock %} -- cgit v1.2.3 From a71c38f07bba0191ba28eef12eea56e8a9265349 Mon Sep 17 00:00:00 2001 From: Leon Sandรธy Date: Sat, 22 Aug 2020 03:54:51 +0200 Subject: Move the SHA into constants.py. The util was redundant. Thanks @MarkKoz --- pydis_site/constants.py | 5 +++++ pydis_site/context_processors.py | 4 ++-- pydis_site/settings.py | 4 ++-- pydis_site/tests/test_utils.py | 11 ----------- pydis_site/utils/__init__.py | 3 --- pydis_site/utils/utils.py | 10 ---------- 6 files changed, 9 insertions(+), 28 deletions(-) create mode 100644 pydis_site/constants.py delete mode 100644 pydis_site/tests/test_utils.py delete mode 100644 pydis_site/utils/__init__.py delete mode 100644 pydis_site/utils/utils.py diff --git a/pydis_site/constants.py b/pydis_site/constants.py new file mode 100644 index 00000000..0b76694a --- /dev/null +++ b/pydis_site/constants.py @@ -0,0 +1,5 @@ +import git + +# Git SHA +repo = git.Repo(search_parent_directories=True) +GIT_SHA = repo.head.object.hexsha diff --git a/pydis_site/context_processors.py b/pydis_site/context_processors.py index ab5a4168..6937a3db 100644 --- a/pydis_site/context_processors.py +++ b/pydis_site/context_processors.py @@ -1,8 +1,8 @@ from django.template import RequestContext -from pydis_site.utils import get_git_sha +from pydis_site.constants import GIT_SHA def git_sha_processor(_: RequestContext) -> dict: """Expose the git SHA for this repo to all views.""" - return {'git_sha': get_git_sha()} + return {'git_sha': GIT_SHA} diff --git a/pydis_site/settings.py b/pydis_site/settings.py index 0a5b0eed..1f042c1b 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -20,7 +20,7 @@ import sentry_sdk from django.contrib.messages import constants as messages from sentry_sdk.integrations.django import DjangoIntegration -from pydis_site.utils import get_git_sha +from pydis_site.constants import GIT_SHA if typing.TYPE_CHECKING: from django.contrib.auth.models import User @@ -35,7 +35,7 @@ sentry_sdk.init( dsn=env('SITE_SENTRY_DSN'), integrations=[DjangoIntegration()], send_default_pii=True, - release=f"pydis-site@{get_git_sha()}" + release=f"pydis-site@{GIT_SHA}" ) # Build paths inside the project like this: os.path.join(BASE_DIR, ...) diff --git a/pydis_site/tests/test_utils.py b/pydis_site/tests/test_utils.py deleted file mode 100644 index f1419860..00000000 --- a/pydis_site/tests/test_utils.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.test import TestCase - -from pydis_site.utils import get_git_sha -from pydis_site.utils.utils import GIT_SHA - - -class UtilsTests(TestCase): - - def test_git_sha(self): - """Test that the get_git_sha returns the correct SHA.""" - self.assertEqual(get_git_sha(), GIT_SHA) diff --git a/pydis_site/utils/__init__.py b/pydis_site/utils/__init__.py deleted file mode 100644 index bb91b3d8..00000000 --- a/pydis_site/utils/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .utils import get_git_sha - -__all__ = ['get_git_sha'] diff --git a/pydis_site/utils/utils.py b/pydis_site/utils/utils.py deleted file mode 100644 index 2033ea19..00000000 --- a/pydis_site/utils/utils.py +++ /dev/null @@ -1,10 +0,0 @@ -import git - -# Git SHA -repo = git.Repo(search_parent_directories=True) -GIT_SHA = repo.head.object.hexsha - - -def get_git_sha() -> str: - """Get the Git SHA for this repo.""" - return GIT_SHA -- cgit v1.2.3 From 464408462cdb737b1eb78777c356253f9d5f0a4a Mon Sep 17 00:00:00 2001 From: rohanjnr Date: Wed, 26 Aug 2020 20:51:05 +0530 Subject: add pagination for GET request on /bot/users endpoint Pagination is done via PageNumberPagination, i.e, each page contains a specific number of `user` objects. --- pydis_site/apps/api/viewsets/bot/user.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 9571b3d7..b016bb66 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -1,3 +1,4 @@ +from rest_framework.pagination import PageNumberPagination from rest_framework.viewsets import ModelViewSet from rest_framework_bulk import BulkCreateModelMixin @@ -5,17 +6,28 @@ from pydis_site.apps.api.models.bot.user import User from pydis_site.apps.api.serializers import UserSerializer +class UserListPagination(PageNumberPagination): + """Custom pagination class for the User Model.""" + + page_size = 10000 + page_size_query_param = "page_size" + + class UserViewSet(BulkCreateModelMixin, ModelViewSet): """ View providing CRUD operations on Discord users through the bot. ## Routes ### GET /bot/users - Returns all users currently known. + Returns all users currently known with pagination. #### Response format - >>> [ - ... { + >>> { + ... 'count': 95000, + ... 'next': "http://api.pythondiscord.com/bot/users?page=2", + ... 'previous': None, + ... 'results': [ + ... { ... 'id': 409107086526644234, ... 'name': "Python", ... 'discriminator': 4329, @@ -26,8 +38,13 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): ... 458226699344019457 ... ], ... 'in_guild': True - ... } - ... ] + ... }, + ... ] + ... } + + #### Query Parameters + - page_size: Number of Users in one page. + - page: Page number #### Status codes - 200: returned on success @@ -118,4 +135,5 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): """ serializer_class = UserSerializer - queryset = User.objects + queryset = User.objects.all() + pagination_class = UserListPagination -- cgit v1.2.3 From 8e636a54b449f44f5bff56577a05d9a6a2dd72c0 Mon Sep 17 00:00:00 2001 From: rohanjnr Date: Wed, 26 Aug 2020 21:59:46 +0530 Subject: add support for bulk updates on user model implemented a method to handle bulk updates on user model via a new endpoint: /bot/users/bulk_patch --- pydis_site/apps/api/serializers.py | 77 +++++++++++++++++++++++++++++++- pydis_site/apps/api/viewsets/bot/user.py | 60 +++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 1 deletion(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 52e0d972..757faeae 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -1,5 +1,13 @@ """Converters from Django models to data interchange formats and back.""" -from rest_framework.serializers import ModelSerializer, PrimaryKeyRelatedField, ValidationError +from django.db.models.query import QuerySet +from rest_framework.serializers import ( + ListSerializer, + ModelSerializer, + PrimaryKeyRelatedField, + ValidationError +) +from rest_framework.settings import api_settings +from rest_framework.utils import html from rest_framework.validators import UniqueTogetherValidator from rest_framework_bulk import BulkSerializerMixin @@ -260,6 +268,72 @@ class TagSerializer(ModelSerializer): fields = ('title', 'embed') +class UserListSerializer(ListSerializer): + """List serializer for User model to handle bulk updates.""" + + def to_internal_value(self, data: list) -> list: + """ + Overriding `to_internal_value` function with a few changes to support bulk updates. + + List of dicts of native values <- List of dicts of primitive datatypes. + """ + if html.is_html_input(data): + data = html.parse_html_list(data, default=[]) + + if not isinstance(data, list): + message = self.error_messages['not_a_list'].format( + input_type=type(data).__name__ + ) + raise ValidationError({ + api_settings.NON_FIELD_ERRORS_KEY: [message] + }, code='not_a_list') + + if not self.allow_empty and len(data) == 0: + message = self.error_messages['empty'] + raise ValidationError({ + api_settings.NON_FIELD_ERRORS_KEY: [message] + }, code='empty') + + ret = [] + errors = [] + + for item in data: + # inserted code + # bug: https://github.com/miki725/django-rest-framework-bulk/issues/68 + # ----------------- + try: + self.child.instance = self.instance.get(id=item['id']) + except User.DoesNotExist: + self.child.instance = None + # ----------------- + self.child.initial_data = item + try: + validated = self.child.run_validation(item) + except ValidationError as exc: + errors.append(exc.detail) + else: + ret.append(validated) + errors.append({}) + + if any(errors): + raise ValidationError(errors) + + return ret + + def update(self, instance: QuerySet, validated_data: list) -> list: + """Override update method to support bulk updates.""" + instance_mapping = {user.id: user for user in instance} + data_mapping = {item['id']: item for item in validated_data} + + updated = [] + for book_id, data in data_mapping.items(): + book = instance_mapping.get(book_id, None) + if book is not None: + updated.append(self.child.update(book, data)) + + return updated + + class UserSerializer(BulkSerializerMixin, ModelSerializer): """A class providing (de-)serialization of `User` instances.""" @@ -269,6 +343,7 @@ class UserSerializer(BulkSerializerMixin, ModelSerializer): model = User fields = ('id', 'name', 'discriminator', 'roles', 'in_guild') depth = 1 + list_serializer_class = UserListSerializer class NominationSerializer(ModelSerializer): diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index b016bb66..d64ca113 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -1,4 +1,8 @@ +from rest_framework import status +from rest_framework.decorators import action from rest_framework.pagination import PageNumberPagination +from rest_framework.request import Request +from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet from rest_framework_bulk import BulkCreateModelMixin @@ -137,3 +141,59 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): serializer_class = UserSerializer queryset = User.objects.all() pagination_class = UserListPagination + + @action(detail=False, methods=["PATCH"]) + def bulk_patch(self, request: Request) -> Response: + """ + Update multiple User objects in a single request. + + ## Route + ### PATCH /bot/users/bulk_patch + Update all users with the IDs. + `id` field is mandatory, rest are optional. + + #### Request body + >>> [ + ... { + ... 'id': int, + ... 'name': str, + ... 'discriminator': int, + ... 'roles': List[int], + ... 'in_guild': bool + ... }, + ... { + ... 'id': int, + ... '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. + """ + queryset = self.get_queryset() + try: + object_ids = [item["id"] for item in request.data] + except KeyError: + # user ID not provided in request body. + resp = { + "Error": "User ID not provided." + } + return Response(resp, status=status.HTTP_400_BAD_REQUEST) + + filtered_instances = queryset.filter(id__in=object_ids) + + serializer = self.get_serializer( + instance=filtered_instances, + data=request.data, + many=True, + partial=True + ) + + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) -- cgit v1.2.3 From 792af719e7934e639535e8803e7cd0118ccf43fb Mon Sep 17 00:00:00 2001 From: rohanjnr Date: Wed, 26 Aug 2020 22:19:24 +0530 Subject: Add reference links in UserListSerializer methods --- pydis_site/apps/api/serializers.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 757faeae..6146584b 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -275,6 +275,8 @@ class UserListSerializer(ListSerializer): """ Overriding `to_internal_value` function with a few changes to support bulk updates. + ref: https://github.com/miki725/django-rest-framework-bulk/issues/68 + List of dicts of native values <- List of dicts of primitive datatypes. """ if html.is_html_input(data): @@ -299,7 +301,6 @@ class UserListSerializer(ListSerializer): for item in data: # inserted code - # bug: https://github.com/miki725/django-rest-framework-bulk/issues/68 # ----------------- try: self.child.instance = self.instance.get(id=item['id']) @@ -321,7 +322,11 @@ class UserListSerializer(ListSerializer): return ret def update(self, instance: QuerySet, validated_data: list) -> list: - """Override update method to support bulk updates.""" + """ + Override update method to support bulk updates. + + ref:https://www.django-rest-framework.org/api-guide/serializers/#customizing-multiple-update + """ instance_mapping = {user.id: user for user in instance} data_mapping = {item['id']: item for item in validated_data} -- cgit v1.2.3 From 303cb191626d330663d64d3a3814e2c215493765 Mon Sep 17 00:00:00 2001 From: rohanjnr Date: Wed, 26 Aug 2020 23:29:32 +0530 Subject: Except AttributeError when self.instance is None and while fetching User object from database. --- pydis_site/apps/api/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 6146584b..0589ce77 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -304,7 +304,7 @@ class UserListSerializer(ListSerializer): # ----------------- try: self.child.instance = self.instance.get(id=item['id']) - except User.DoesNotExist: + except (User.DoesNotExist, AttributeError): self.child.instance = None # ----------------- self.child.initial_data = item -- cgit v1.2.3 From d52a20962bbda8f94646ff12c38eb71e2a5241ab Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 08:38:25 +0300 Subject: (Tag Cleanup): Removed Tags viewset. --- pydis_site/apps/api/viewsets/bot/__init__.py | 1 - pydis_site/apps/api/viewsets/bot/tag.py | 105 --------------------------- 2 files changed, 106 deletions(-) delete mode 100644 pydis_site/apps/api/viewsets/bot/tag.py diff --git a/pydis_site/apps/api/viewsets/bot/__init__.py b/pydis_site/apps/api/viewsets/bot/__init__.py index e64e3988..84b87eab 100644 --- a/pydis_site/apps/api/viewsets/bot/__init__.py +++ b/pydis_site/apps/api/viewsets/bot/__init__.py @@ -9,5 +9,4 @@ from .off_topic_channel_name import OffTopicChannelNameViewSet from .offensive_message import OffensiveMessageViewSet from .reminder import ReminderViewSet from .role import RoleViewSet -from .tag import TagViewSet from .user import UserViewSet diff --git a/pydis_site/apps/api/viewsets/bot/tag.py b/pydis_site/apps/api/viewsets/bot/tag.py deleted file mode 100644 index 7e9ba117..00000000 --- a/pydis_site/apps/api/viewsets/bot/tag.py +++ /dev/null @@ -1,105 +0,0 @@ -from rest_framework.viewsets import ModelViewSet - -from pydis_site.apps.api.models.bot.tag import Tag -from pydis_site.apps.api.serializers import TagSerializer - - -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() -- cgit v1.2.3 From 86409cf41469bd2e5b8d69988a729b3b88bb0a26 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 08:39:37 +0300 Subject: (Tag Cleanup): Removed Tags from Administration --- pydis_site/apps/api/admin.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index 0333fefc..dd1291b8 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -14,7 +14,6 @@ from .models import ( OffTopicChannelName, OffensiveMessage, Role, - Tag, User ) @@ -64,5 +63,4 @@ admin.site.register(Nomination) admin.site.register(OffensiveMessage) admin.site.register(OffTopicChannelName) admin.site.register(Role) -admin.site.register(Tag) admin.site.register(User) -- cgit v1.2.3 From 14453c9607ed83e040c9b1ed000a31e47568fd69 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 08:41:16 +0300 Subject: (Tag Cleanup): Removed Tags serializer. --- pydis_site/apps/api/serializers.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 52e0d972..90bd6f91 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -16,7 +16,6 @@ from .models import ( OffensiveMessage, Reminder, Role, - Tag, User ) @@ -250,16 +249,6 @@ class RoleSerializer(ModelSerializer): fields = ('id', 'name', 'colour', 'permissions', 'position') -class TagSerializer(ModelSerializer): - """A class providing (de-)serialization of `Tag` instances.""" - - class Meta: - """Metadata defined for the Django REST Framework.""" - - model = Tag - fields = ('title', 'embed') - - class UserSerializer(BulkSerializerMixin, ModelSerializer): """A class providing (de-)serialization of `User` instances.""" -- cgit v1.2.3 From 05b0c725b45dd87c755a8475d08442a0603a515e Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 08:41:58 +0300 Subject: (Tag Cleanup): Removed Tags URL --- pydis_site/apps/api/urls.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pydis_site/apps/api/urls.py b/pydis_site/apps/api/urls.py index a4fd5b2e..4dbf93db 100644 --- a/pydis_site/apps/api/urls.py +++ b/pydis_site/apps/api/urls.py @@ -14,7 +14,6 @@ from .viewsets import ( OffensiveMessageViewSet, ReminderViewSet, RoleViewSet, - TagViewSet, UserViewSet ) @@ -61,10 +60,6 @@ bot_router.register( 'roles', RoleViewSet ) -bot_router.register( - 'tags', - TagViewSet -) bot_router.register( 'users', UserViewSet -- cgit v1.2.3 From bb55875dcdaf838c41b1e3b2c3dc4fdf368b0f09 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 08:43:33 +0300 Subject: (Tag Cleanup): Removed Tags Model test. --- pydis_site/apps/api/tests/test_models.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pydis_site/apps/api/tests/test_models.py b/pydis_site/apps/api/tests/test_models.py index e0e347bb..853e6621 100644 --- a/pydis_site/apps/api/tests/test_models.py +++ b/pydis_site/apps/api/tests/test_models.py @@ -14,7 +14,6 @@ from pydis_site.apps.api.models import ( OffensiveMessage, Reminder, Role, - Tag, User ) from pydis_site.apps.api.models.mixins import ModelReprMixin @@ -104,10 +103,6 @@ class StringDunderMethodTests(SimpleTestCase): ), creation=dt.utcnow() ), - Tag( - title='bob', - embed={'content': "the builder"} - ), User( id=5, name='bob', -- cgit v1.2.3 From 790c50cbc2d45134fc0b668bc21ca7e01e4af2a2 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 08:46:19 +0300 Subject: (Tag Cleanup): Removed Tags Model import from models __init__.py --- pydis_site/apps/api/models/bot/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pydis_site/apps/api/models/bot/__init__.py b/pydis_site/apps/api/models/bot/__init__.py index efd98184..1673b434 100644 --- a/pydis_site/apps/api/models/bot/__init__.py +++ b/pydis_site/apps/api/models/bot/__init__.py @@ -11,5 +11,4 @@ from .off_topic_channel_name import OffTopicChannelName from .offensive_message import OffensiveMessage from .reminder import Reminder from .role import Role -from .tag import Tag from .user import User -- cgit v1.2.3 From b16809620f444872f4d295ee55e4e8e9cdde12c8 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 08:49:49 +0300 Subject: (Tag Cleanup): Moved embed validators from Tag model to utils.py --- pydis_site/apps/api/models/mixins.py | 173 +++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py index 5d75b78b..2d658cb9 100644 --- a/pydis_site/apps/api/models/mixins.py +++ b/pydis_site/apps/api/models/mixins.py @@ -1,4 +1,177 @@ +from collections.abc import Mapping from operator import itemgetter +from typing import Any, Dict + +from django.core.exceptions import ValidationError +from django.core.validators import MaxLengthValidator, MinLengthValidator + + +def is_bool_validator(value: Any) -> None: + """Validates if a given value is of type bool.""" + if not isinstance(value, bool): + raise ValidationError(f"This field must be of type bool, not {type(value)}.") + + +def validate_tag_embed_fields(fields: dict) -> None: + """Raises a ValidationError if any of the given embed fields is invalid.""" + field_validators = { + 'name': (MaxLengthValidator(limit_value=256),), + 'value': (MaxLengthValidator(limit_value=1024),), + 'inline': (is_bool_validator,), + } + + required_fields = ('name', 'value') + + for field in fields: + if not isinstance(field, Mapping): + raise ValidationError("Embed fields must be a mapping.") + + if not all(required_field in field for required_field in required_fields): + raise ValidationError( + f"Embed fields must contain the following fields: {', '.join(required_fields)}." + ) + + for field_name, value in field.items(): + if field_name not in field_validators: + raise ValidationError(f"Unknown embed field field: {field_name!r}.") + + for validator in field_validators[field_name]: + validator(value) + + +def validate_tag_embed_footer(footer: Dict[str, str]) -> None: + """Raises a ValidationError if the given footer is invalid.""" + field_validators = { + 'text': ( + MinLengthValidator( + limit_value=1, + message="Footer text must not be empty." + ), + MaxLengthValidator(limit_value=2048) + ), + 'icon_url': (), + 'proxy_icon_url': () + } + + if not isinstance(footer, Mapping): + raise ValidationError("Embed footer must be a mapping.") + + for field_name, value in footer.items(): + if field_name not in field_validators: + raise ValidationError(f"Unknown embed footer field: {field_name!r}.") + + for validator in field_validators[field_name]: + validator(value) + + +def validate_tag_embed_author(author: Any) -> None: + """Raises a ValidationError if the given author is invalid.""" + field_validators = { + 'name': ( + MinLengthValidator( + limit_value=1, + message="Embed author name must not be empty." + ), + MaxLengthValidator(limit_value=256) + ), + 'url': (), + 'icon_url': (), + 'proxy_icon_url': () + } + + if not isinstance(author, Mapping): + raise ValidationError("Embed author must be a mapping.") + + for field_name, value in author.items(): + if field_name not in field_validators: + raise ValidationError(f"Unknown embed author field: {field_name!r}.") + + for validator in field_validators[field_name]: + validator(value) + + +def validate_tag_embed(embed: Any) -> None: + """ + Validate a JSON document containing an embed as possible to send on Discord. + + This attempts to rebuild the validation used by Discord + as well as possible by checking for various embed limits so we can + ensure that any embed we store here will also be accepted as a + valid embed by the Discord API. + + Using this directly is possible, although not intended - you usually + stick this onto the `validators` keyword argument of model fields. + + Example: + + >>> from django.contrib.postgres import fields as pgfields + >>> from django.db import models + >>> from pydis_site.apps.api.models.bot.tag import validate_tag_embed + >>> class MyMessage(models.Model): + ... embed = pgfields.JSONField( + ... validators=( + ... validate_tag_embed, + ... ) + ... ) + ... # ... + ... + + Args: + embed (Any): + A dictionary describing the contents of this embed. + See the official documentation for a full reference + of accepted keys by this dictionary: + https://discordapp.com/developers/docs/resources/channel#embed-object + + Raises: + ValidationError: + In case the given embed is deemed invalid, a `ValidationError` + is raised which in turn will allow Django to display errors + as appropriate. + """ + all_keys = { + 'title', 'type', 'description', 'url', 'timestamp', + 'color', 'footer', 'image', 'thumbnail', 'video', + 'provider', 'author', 'fields' + } + one_required_of = {'description', 'fields', 'image', 'title', 'video'} + field_validators = { + 'title': ( + MinLengthValidator( + limit_value=1, + message="Embed title must not be empty." + ), + MaxLengthValidator(limit_value=256) + ), + 'description': (MaxLengthValidator(limit_value=2048),), + 'fields': ( + MaxLengthValidator(limit_value=25), + validate_tag_embed_fields + ), + 'footer': (validate_tag_embed_footer,), + 'author': (validate_tag_embed_author,) + } + + if not embed: + raise ValidationError("Tag embed must not be empty.") + + elif not isinstance(embed, Mapping): + raise ValidationError("Tag embed must be a mapping.") + + elif not any(field in embed for field in one_required_of): + raise ValidationError(f"Tag embed must contain one of the fields {one_required_of}.") + + for required_key in one_required_of: + if required_key in embed and not embed[required_key]: + raise ValidationError(f"Key {required_key!r} must not be empty.") + + for field_name, value in embed.items(): + if field_name not in all_keys: + raise ValidationError(f"Unknown field name: {field_name!r}") + + if field_name in field_validators: + for validator in field_validators[field_name]: + validator(value) from django.db import models -- cgit v1.2.3 From 1f623b6629bdbc854323dc71d149a647355031c6 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 08:52:05 +0300 Subject: (Tag Cleanup): Removed `tag` from embed validator names due Tags will be removed. --- pydis_site/apps/api/models/mixins.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py index 2d658cb9..e95888b7 100644 --- a/pydis_site/apps/api/models/mixins.py +++ b/pydis_site/apps/api/models/mixins.py @@ -12,7 +12,7 @@ def is_bool_validator(value: Any) -> None: raise ValidationError(f"This field must be of type bool, not {type(value)}.") -def validate_tag_embed_fields(fields: dict) -> None: +def validate_embed_fields(fields: dict) -> None: """Raises a ValidationError if any of the given embed fields is invalid.""" field_validators = { 'name': (MaxLengthValidator(limit_value=256),), @@ -39,7 +39,7 @@ def validate_tag_embed_fields(fields: dict) -> None: validator(value) -def validate_tag_embed_footer(footer: Dict[str, str]) -> None: +def validate_embed_footer(footer: Dict[str, str]) -> None: """Raises a ValidationError if the given footer is invalid.""" field_validators = { 'text': ( @@ -64,7 +64,7 @@ def validate_tag_embed_footer(footer: Dict[str, str]) -> None: validator(value) -def validate_tag_embed_author(author: Any) -> None: +def validate_embed_author(author: Any) -> None: """Raises a ValidationError if the given author is invalid.""" field_validators = { 'name': ( @@ -90,7 +90,7 @@ def validate_tag_embed_author(author: Any) -> None: validator(value) -def validate_tag_embed(embed: Any) -> None: +def validate_embed(embed: Any) -> None: """ Validate a JSON document containing an embed as possible to send on Discord. @@ -106,11 +106,11 @@ def validate_tag_embed(embed: Any) -> None: >>> from django.contrib.postgres import fields as pgfields >>> from django.db import models - >>> from pydis_site.apps.api.models.bot.tag import validate_tag_embed + >>> from pydis_site.apps.api.models.utils import validate_embed >>> class MyMessage(models.Model): ... embed = pgfields.JSONField( ... validators=( - ... validate_tag_embed, + ... validate_embed, ... ) ... ) ... # ... @@ -146,10 +146,10 @@ def validate_tag_embed(embed: Any) -> None: 'description': (MaxLengthValidator(limit_value=2048),), 'fields': ( MaxLengthValidator(limit_value=25), - validate_tag_embed_fields + validate_embed_fields ), - 'footer': (validate_tag_embed_footer,), - 'author': (validate_tag_embed_author,) + 'footer': (validate_embed_footer,), + 'author': (validate_embed_author,) } if not embed: -- cgit v1.2.3 From 9b6b9d9d01811bbe2d9a1964970e3b4a38180623 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 08:55:09 +0300 Subject: (Tag Cleanup): Replaced import from Tags model with utils.py validator import. --- pydis_site/apps/api/models/bot/message.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pydis_site/apps/api/models/bot/message.py b/pydis_site/apps/api/models/bot/message.py index 78dcbf1d..e929ea25 100644 --- a/pydis_site/apps/api/models/bot/message.py +++ b/pydis_site/apps/api/models/bot/message.py @@ -5,9 +5,8 @@ from django.core.validators import MinValueValidator from django.db import models from django.utils import timezone -from pydis_site.apps.api.models.bot.tag import validate_tag_embed from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.mixins import ModelReprMixin +from pydis_site.apps.api.models.utils import ModelReprMixin, validate_embed class Message(ModelReprMixin, models.Model): @@ -47,7 +46,7 @@ class Message(ModelReprMixin, models.Model): ) embeds = pgfields.ArrayField( pgfields.JSONField( - validators=(validate_tag_embed,) + validators=(validate_embed,) ), blank=True, help_text="Embeds attached to this message." -- cgit v1.2.3 From 1029c56eb7e1f986a540a58bc2e657db6925a93a Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 08:57:27 +0300 Subject: (Tag Cleanup): Replaced import from Tags model with utils.py validator import in validators test. --- pydis_site/apps/api/tests/test_validators.py | 56 ++++++++++++++-------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/pydis_site/apps/api/tests/test_validators.py b/pydis_site/apps/api/tests/test_validators.py index 241af08c..8bb7b917 100644 --- a/pydis_site/apps/api/tests/test_validators.py +++ b/pydis_site/apps/api/tests/test_validators.py @@ -5,7 +5,7 @@ from django.test import TestCase from ..models.bot.bot_setting import validate_bot_setting_name from ..models.bot.offensive_message import future_date_validator -from ..models.bot.tag import validate_tag_embed +from ..models.utils import validate_embed REQUIRED_KEYS = ( @@ -25,77 +25,77 @@ class BotSettingValidatorTests(TestCase): class TagEmbedValidatorTests(TestCase): def test_rejects_non_mapping(self): with self.assertRaises(ValidationError): - validate_tag_embed('non-empty non-mapping') + validate_embed('non-empty non-mapping') def test_rejects_missing_required_keys(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'unknown': "key" }) def test_rejects_one_correct_one_incorrect(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'provider': "??", 'title': "" }) def test_rejects_empty_required_key(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'title': '' }) def test_rejects_list_as_embed(self): with self.assertRaises(ValidationError): - validate_tag_embed([]) + validate_embed([]) def test_rejects_required_keys_and_unknown_keys(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'title': "the duck walked up to the lemonade stand", 'and': "he said to the man running the stand" }) def test_rejects_too_long_title(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'title': 'a' * 257 }) def test_rejects_too_many_fields(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'fields': [{} for _ in range(26)] }) def test_rejects_too_long_description(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'description': 'd' * 2049 }) def test_allows_valid_embed(self): - validate_tag_embed({ + validate_embed({ 'title': "My embed", 'description': "look at my embed, my embed is amazing" }) def test_allows_unvalidated_fields(self): - validate_tag_embed({ + validate_embed({ 'title': "My embed", 'provider': "what am I??" }) def test_rejects_fields_as_list_of_non_mappings(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'fields': ['abc'] }) def test_rejects_fields_with_unknown_fields(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'fields': [ { 'what': "is this field" @@ -105,7 +105,7 @@ class TagEmbedValidatorTests(TestCase): def test_rejects_fields_with_too_long_name(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'fields': [ { 'name': "a" * 257 @@ -115,7 +115,7 @@ class TagEmbedValidatorTests(TestCase): def test_rejects_one_correct_one_incorrect_field(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'fields': [ { 'name': "Totally valid", @@ -131,7 +131,7 @@ class TagEmbedValidatorTests(TestCase): def test_rejects_missing_required_field_field(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'fields': [ { 'name': "Totally valid", @@ -142,7 +142,7 @@ class TagEmbedValidatorTests(TestCase): def test_rejects_invalid_inline_field_field(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'fields': [ { 'name': "Totally valid", @@ -153,7 +153,7 @@ class TagEmbedValidatorTests(TestCase): }) def test_allows_valid_fields(self): - validate_tag_embed({ + validate_embed({ 'fields': [ { 'name': "valid", @@ -174,14 +174,14 @@ class TagEmbedValidatorTests(TestCase): def test_rejects_footer_as_non_mapping(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'title': "whatever", 'footer': [] }) def test_rejects_footer_with_unknown_fields(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'title': "whatever", 'footer': { 'duck': "quack" @@ -190,7 +190,7 @@ class TagEmbedValidatorTests(TestCase): def test_rejects_footer_with_empty_text(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'title': "whatever", 'footer': { 'text': "" @@ -198,7 +198,7 @@ class TagEmbedValidatorTests(TestCase): }) def test_allows_footer_with_proper_values(self): - validate_tag_embed({ + validate_embed({ 'title': "whatever", 'footer': { 'text': "django good" @@ -207,14 +207,14 @@ class TagEmbedValidatorTests(TestCase): def test_rejects_author_as_non_mapping(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'title': "whatever", 'author': [] }) def test_rejects_author_with_unknown_field(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'title': "whatever", 'author': { 'field': "that is unknown" @@ -223,7 +223,7 @@ class TagEmbedValidatorTests(TestCase): def test_rejects_author_with_empty_name(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'title': "whatever", 'author': { 'name': "" @@ -232,7 +232,7 @@ class TagEmbedValidatorTests(TestCase): def test_rejects_author_with_one_correct_one_incorrect(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'title': "whatever", 'author': { # Relies on "dictionary insertion order remembering" (D.I.O.R.) behaviour @@ -242,7 +242,7 @@ class TagEmbedValidatorTests(TestCase): }) def test_allows_author_with_proper_values(self): - validate_tag_embed({ + validate_embed({ 'title': "whatever", 'author': { 'name': "Bob" -- cgit v1.2.3 From 5df2d613bc72aad60fed73d2703dff55093def03 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 09:02:48 +0300 Subject: (Tag Cleanup): Removed tag import in models __init__.py --- pydis_site/apps/api/models/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py index 1d0ab7ea..e3f928e1 100644 --- a/pydis_site/apps/api/models/__init__.py +++ b/pydis_site/apps/api/models/__init__.py @@ -12,7 +12,6 @@ from .bot import ( OffTopicChannelName, Reminder, Role, - Tag, User ) from .log_entry import LogEntry -- cgit v1.2.3 From 494b735f762bf1780448db4591a4be4850dcdd4a Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 09:17:43 +0300 Subject: (Tag Cleanup): Removed Tag model --- .../apps/api/migrations/0019_deletedmessage.py | 2 +- pydis_site/apps/api/models/bot/tag.py | 25 ---------------------- 2 files changed, 1 insertion(+), 26 deletions(-) diff --git a/pydis_site/apps/api/migrations/0019_deletedmessage.py b/pydis_site/apps/api/migrations/0019_deletedmessage.py index 33746253..6b848d64 100644 --- a/pydis_site/apps/api/migrations/0019_deletedmessage.py +++ b/pydis_site/apps/api/migrations/0019_deletedmessage.py @@ -18,7 +18,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.models.bot.tag.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.models.utils.validate_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/models/bot/tag.py b/pydis_site/apps/api/models/bot/tag.py index 5e53582f..790ad37a 100644 --- a/pydis_site/apps/api/models/bot/tag.py +++ b/pydis_site/apps/api/models/bot/tag.py @@ -1,12 +1,8 @@ from collections.abc import Mapping from typing import Any, Dict -from django.contrib.postgres import fields as pgfields from django.core.exceptions import ValidationError from django.core.validators import MaxLengthValidator, MinLengthValidator -from django.db import models - -from pydis_site.apps.api.models.mixins import ModelReprMixin def is_bool_validator(value: Any) -> None: @@ -175,24 +171,3 @@ def validate_tag_embed(embed: Any) -> None: if field_name in field_validators: for validator in field_validators[field_name]: validator(value) - - -class Tag(ModelReprMixin, models.Model): - """A tag providing (hopefully) useful information.""" - - title = models.CharField( - max_length=100, - help_text=( - "The title of this tag, shown in searches and providing " - "a quick overview over what this embed contains." - ), - primary_key=True - ) - embed = pgfields.JSONField( - help_text="The actual embed shown by this tag.", - validators=(validate_tag_embed,) - ) - - def __str__(self): - """Returns the title of this tag, for display purposes.""" - return self.title -- cgit v1.2.3 From ad5183a3956321c8956defa3f32e399400662bf6 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 09:20:48 +0300 Subject: (Tag Cleanup): Removed unnecessary tag validation migration. --- pydis_site/apps/api/migrations/0008_tag_embed_validator.py | 7 ------- 1 file changed, 7 deletions(-) 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 d53ddb90..d92042d2 100644 --- a/pydis_site/apps/api/migrations/0008_tag_embed_validator.py +++ b/pydis_site/apps/api/migrations/0008_tag_embed_validator.py @@ -1,7 +1,5 @@ # Generated by Django 2.1.1 on 2018-09-23 10:07 -import pydis_site.apps.api.models.bot.tag -import django.contrib.postgres.fields.jsonb from django.db import migrations @@ -12,9 +10,4 @@ class Migration(migrations.Migration): ] operations = [ - 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.models.bot.tag.validate_tag_embed]), - ), ] -- cgit v1.2.3 From a11d3a38a03b63a2018785917a56fe54c1b1f027 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 09:21:17 +0300 Subject: (Tag Cleanup): Removed Tag model file. --- pydis_site/apps/api/models/bot/tag.py | 173 ---------------------------------- 1 file changed, 173 deletions(-) delete mode 100644 pydis_site/apps/api/models/bot/tag.py diff --git a/pydis_site/apps/api/models/bot/tag.py b/pydis_site/apps/api/models/bot/tag.py deleted file mode 100644 index 790ad37a..00000000 --- a/pydis_site/apps/api/models/bot/tag.py +++ /dev/null @@ -1,173 +0,0 @@ -from collections.abc import Mapping -from typing import Any, Dict - -from django.core.exceptions import ValidationError -from django.core.validators import MaxLengthValidator, MinLengthValidator - - -def is_bool_validator(value: Any) -> None: - """Validates if a given value is of type bool.""" - if not isinstance(value, bool): - raise ValidationError(f"This field must be of type bool, not {type(value)}.") - - -def validate_tag_embed_fields(fields: dict) -> None: - """Raises a ValidationError if any of the given embed fields is invalid.""" - field_validators = { - 'name': (MaxLengthValidator(limit_value=256),), - 'value': (MaxLengthValidator(limit_value=1024),), - 'inline': (is_bool_validator,), - } - - required_fields = ('name', 'value') - - for field in fields: - if not isinstance(field, Mapping): - raise ValidationError("Embed fields must be a mapping.") - - if not all(required_field in field for required_field in required_fields): - raise ValidationError( - f"Embed fields must contain the following fields: {', '.join(required_fields)}." - ) - - for field_name, value in field.items(): - if field_name not in field_validators: - raise ValidationError(f"Unknown embed field field: {field_name!r}.") - - for validator in field_validators[field_name]: - validator(value) - - -def validate_tag_embed_footer(footer: Dict[str, str]) -> None: - """Raises a ValidationError if the given footer is invalid.""" - field_validators = { - 'text': ( - MinLengthValidator( - limit_value=1, - message="Footer text must not be empty." - ), - MaxLengthValidator(limit_value=2048) - ), - 'icon_url': (), - 'proxy_icon_url': () - } - - if not isinstance(footer, Mapping): - raise ValidationError("Embed footer must be a mapping.") - - for field_name, value in footer.items(): - if field_name not in field_validators: - raise ValidationError(f"Unknown embed footer field: {field_name!r}.") - - for validator in field_validators[field_name]: - validator(value) - - -def validate_tag_embed_author(author: Any) -> None: - """Raises a ValidationError if the given author is invalid.""" - field_validators = { - 'name': ( - MinLengthValidator( - limit_value=1, - message="Embed author name must not be empty." - ), - MaxLengthValidator(limit_value=256) - ), - 'url': (), - 'icon_url': (), - 'proxy_icon_url': () - } - - if not isinstance(author, Mapping): - raise ValidationError("Embed author must be a mapping.") - - for field_name, value in author.items(): - if field_name not in field_validators: - raise ValidationError(f"Unknown embed author field: {field_name!r}.") - - for validator in field_validators[field_name]: - validator(value) - - -def validate_tag_embed(embed: Any) -> None: - """ - Validate a JSON document containing an embed as possible to send on Discord. - - This attempts to rebuild the validation used by Discord - as well as possible by checking for various embed limits so we can - ensure that any embed we store here will also be accepted as a - valid embed by the Discord API. - - Using this directly is possible, although not intended - you usually - stick this onto the `validators` keyword argument of model fields. - - Example: - - >>> from django.contrib.postgres import fields as pgfields - >>> from django.db import models - >>> from pydis_site.apps.api.models.bot.tag import validate_tag_embed - >>> class MyMessage(models.Model): - ... embed = pgfields.JSONField( - ... validators=( - ... validate_tag_embed, - ... ) - ... ) - ... # ... - ... - - Args: - embed (Any): - A dictionary describing the contents of this embed. - See the official documentation for a full reference - of accepted keys by this dictionary: - https://discordapp.com/developers/docs/resources/channel#embed-object - - Raises: - ValidationError: - In case the given embed is deemed invalid, a `ValidationError` - is raised which in turn will allow Django to display errors - as appropriate. - """ - all_keys = { - 'title', 'type', 'description', 'url', 'timestamp', - 'color', 'footer', 'image', 'thumbnail', 'video', - 'provider', 'author', 'fields' - } - one_required_of = {'description', 'fields', 'image', 'title', 'video'} - field_validators = { - 'title': ( - MinLengthValidator( - limit_value=1, - message="Embed title must not be empty." - ), - MaxLengthValidator(limit_value=256) - ), - 'description': (MaxLengthValidator(limit_value=2048),), - 'fields': ( - MaxLengthValidator(limit_value=25), - validate_tag_embed_fields - ), - 'footer': (validate_tag_embed_footer,), - 'author': (validate_tag_embed_author,) - } - - if not embed: - raise ValidationError("Tag embed must not be empty.") - - elif not isinstance(embed, Mapping): - raise ValidationError("Tag embed must be a mapping.") - - elif not any(field in embed for field in one_required_of): - raise ValidationError(f"Tag embed must contain one of the fields {one_required_of}.") - - for required_key in one_required_of: - if required_key in embed and not embed[required_key]: - raise ValidationError(f"Key {required_key!r} must not be empty.") - - for field_name, value in embed.items(): - if field_name not in all_keys: - raise ValidationError(f"Unknown field name: {field_name!r}") - - if field_name in field_validators: - for validator in field_validators[field_name]: - validator(value) -- cgit v1.2.3 From 5357d5baf3234550c09f88fa38a9f436503f570c Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 09:21:48 +0300 Subject: (Tag Cleanup): Added Tag removal migration --- pydis_site/apps/api/migrations/0051_delete_tag.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0051_delete_tag.py diff --git a/pydis_site/apps/api/migrations/0051_delete_tag.py b/pydis_site/apps/api/migrations/0051_delete_tag.py new file mode 100644 index 00000000..bada5788 --- /dev/null +++ b/pydis_site/apps/api/migrations/0051_delete_tag.py @@ -0,0 +1,16 @@ +# Generated by Django 2.2.11 on 2020-04-01 06:15 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0050_remove_infractions_active_default_value'), + ] + + operations = [ + migrations.DeleteModel( + name='Tag', + ), + ] -- cgit v1.2.3 From 99485facba10d1f0d918e8c0ab152372db89c7b9 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 09:25:15 +0300 Subject: (Tag Cleanup): Removed Tag viewset from viewsets __init__.py --- pydis_site/apps/api/viewsets/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pydis_site/apps/api/viewsets/__init__.py b/pydis_site/apps/api/viewsets/__init__.py index 8699517e..dfbb880d 100644 --- a/pydis_site/apps/api/viewsets/__init__.py +++ b/pydis_site/apps/api/viewsets/__init__.py @@ -10,7 +10,6 @@ from .bot import ( OffTopicChannelNameViewSet, ReminderViewSet, RoleViewSet, - TagViewSet, UserViewSet ) from .log_entry import LogEntryViewSet -- cgit v1.2.3 From 2b2f491e476c4564899c8716fde189da4a493287 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 27 Aug 2020 07:34:41 +0300 Subject: Move import to beginning of models mixins file --- pydis_site/apps/api/models/mixins.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py index e95888b7..692d14f7 100644 --- a/pydis_site/apps/api/models/mixins.py +++ b/pydis_site/apps/api/models/mixins.py @@ -4,6 +4,7 @@ from typing import Any, Dict from django.core.exceptions import ValidationError from django.core.validators import MaxLengthValidator, MinLengthValidator +from django.db import models def is_bool_validator(value: Any) -> None: @@ -173,8 +174,6 @@ def validate_embed(embed: Any) -> None: for validator in field_validators[field_name]: validator(value) -from django.db import models - class ModelReprMixin: """Mixin providing a `__repr__()` to display model class name and initialisation parameters.""" -- cgit v1.2.3 From 2011d2f2c30dc67c1f34e82e5593f8e4ce1243dd Mon Sep 17 00:00:00 2001 From: Karlis S Date: Thu, 27 Aug 2020 04:44:08 +0000 Subject: Move some parts from Mixins file to utils --- pydis_site/apps/api/models/mixins.py | 172 ---------------------------------- pydis_site/apps/api/models/utils.py | 174 +++++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+), 172 deletions(-) create mode 100644 pydis_site/apps/api/models/utils.py diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py index 692d14f7..5d75b78b 100644 --- a/pydis_site/apps/api/models/mixins.py +++ b/pydis_site/apps/api/models/mixins.py @@ -1,180 +1,8 @@ -from collections.abc import Mapping from operator import itemgetter -from typing import Any, Dict -from django.core.exceptions import ValidationError -from django.core.validators import MaxLengthValidator, MinLengthValidator from django.db import models -def is_bool_validator(value: Any) -> None: - """Validates if a given value is of type bool.""" - if not isinstance(value, bool): - raise ValidationError(f"This field must be of type bool, not {type(value)}.") - - -def validate_embed_fields(fields: dict) -> None: - """Raises a ValidationError if any of the given embed fields is invalid.""" - field_validators = { - 'name': (MaxLengthValidator(limit_value=256),), - 'value': (MaxLengthValidator(limit_value=1024),), - 'inline': (is_bool_validator,), - } - - required_fields = ('name', 'value') - - for field in fields: - if not isinstance(field, Mapping): - raise ValidationError("Embed fields must be a mapping.") - - if not all(required_field in field for required_field in required_fields): - raise ValidationError( - f"Embed fields must contain the following fields: {', '.join(required_fields)}." - ) - - for field_name, value in field.items(): - if field_name not in field_validators: - raise ValidationError(f"Unknown embed field field: {field_name!r}.") - - for validator in field_validators[field_name]: - validator(value) - - -def validate_embed_footer(footer: Dict[str, str]) -> None: - """Raises a ValidationError if the given footer is invalid.""" - field_validators = { - 'text': ( - MinLengthValidator( - limit_value=1, - message="Footer text must not be empty." - ), - MaxLengthValidator(limit_value=2048) - ), - 'icon_url': (), - 'proxy_icon_url': () - } - - if not isinstance(footer, Mapping): - raise ValidationError("Embed footer must be a mapping.") - - for field_name, value in footer.items(): - if field_name not in field_validators: - raise ValidationError(f"Unknown embed footer field: {field_name!r}.") - - for validator in field_validators[field_name]: - validator(value) - - -def validate_embed_author(author: Any) -> None: - """Raises a ValidationError if the given author is invalid.""" - field_validators = { - 'name': ( - MinLengthValidator( - limit_value=1, - message="Embed author name must not be empty." - ), - MaxLengthValidator(limit_value=256) - ), - 'url': (), - 'icon_url': (), - 'proxy_icon_url': () - } - - if not isinstance(author, Mapping): - raise ValidationError("Embed author must be a mapping.") - - for field_name, value in author.items(): - if field_name not in field_validators: - raise ValidationError(f"Unknown embed author field: {field_name!r}.") - - for validator in field_validators[field_name]: - validator(value) - - -def validate_embed(embed: Any) -> None: - """ - Validate a JSON document containing an embed as possible to send on Discord. - - This attempts to rebuild the validation used by Discord - as well as possible by checking for various embed limits so we can - ensure that any embed we store here will also be accepted as a - valid embed by the Discord API. - - Using this directly is possible, although not intended - you usually - stick this onto the `validators` keyword argument of model fields. - - Example: - - >>> from django.contrib.postgres import fields as pgfields - >>> from django.db import models - >>> from pydis_site.apps.api.models.utils import validate_embed - >>> class MyMessage(models.Model): - ... embed = pgfields.JSONField( - ... validators=( - ... validate_embed, - ... ) - ... ) - ... # ... - ... - - Args: - embed (Any): - A dictionary describing the contents of this embed. - See the official documentation for a full reference - of accepted keys by this dictionary: - https://discordapp.com/developers/docs/resources/channel#embed-object - - Raises: - ValidationError: - In case the given embed is deemed invalid, a `ValidationError` - is raised which in turn will allow Django to display errors - as appropriate. - """ - all_keys = { - 'title', 'type', 'description', 'url', 'timestamp', - 'color', 'footer', 'image', 'thumbnail', 'video', - 'provider', 'author', 'fields' - } - one_required_of = {'description', 'fields', 'image', 'title', 'video'} - field_validators = { - 'title': ( - MinLengthValidator( - limit_value=1, - message="Embed title must not be empty." - ), - MaxLengthValidator(limit_value=256) - ), - 'description': (MaxLengthValidator(limit_value=2048),), - 'fields': ( - MaxLengthValidator(limit_value=25), - validate_embed_fields - ), - 'footer': (validate_embed_footer,), - 'author': (validate_embed_author,) - } - - if not embed: - raise ValidationError("Tag embed must not be empty.") - - elif not isinstance(embed, Mapping): - raise ValidationError("Tag embed must be a mapping.") - - elif not any(field in embed for field in one_required_of): - raise ValidationError(f"Tag embed must contain one of the fields {one_required_of}.") - - for required_key in one_required_of: - if required_key in embed and not embed[required_key]: - raise ValidationError(f"Key {required_key!r} must not be empty.") - - for field_name, value in embed.items(): - if field_name not in all_keys: - raise ValidationError(f"Unknown field name: {field_name!r}") - - if field_name in field_validators: - for validator in field_validators[field_name]: - validator(value) - - class ModelReprMixin: """Mixin providing a `__repr__()` to display model class name and initialisation parameters.""" diff --git a/pydis_site/apps/api/models/utils.py b/pydis_site/apps/api/models/utils.py new file mode 100644 index 00000000..97a79507 --- /dev/null +++ b/pydis_site/apps/api/models/utils.py @@ -0,0 +1,174 @@ +from collections.abc import Mapping +from operator import itemgetter +from typing import Any, Dict + +from django.core.exceptions import ValidationError +from django.core.validators import MaxLengthValidator, MinLengthValidator + + +def is_bool_validator(value: Any) -> None: + """Validates if a given value is of type bool.""" + if not isinstance(value, bool): + raise ValidationError(f"This field must be of type bool, not {type(value)}.") + + +def validate_embed_fields(fields: dict) -> None: + """Raises a ValidationError if any of the given embed fields is invalid.""" + field_validators = { + 'name': (MaxLengthValidator(limit_value=256),), + 'value': (MaxLengthValidator(limit_value=1024),), + 'inline': (is_bool_validator,), + } + + required_fields = ('name', 'value') + + for field in fields: + if not isinstance(field, Mapping): + raise ValidationError("Embed fields must be a mapping.") + + if not all(required_field in field for required_field in required_fields): + raise ValidationError( + f"Embed fields must contain the following fields: {', '.join(required_fields)}." + ) + + for field_name, value in field.items(): + if field_name not in field_validators: + raise ValidationError(f"Unknown embed field field: {field_name!r}.") + + for validator in field_validators[field_name]: + validator(value) + + +def validate_embed_footer(footer: Dict[str, str]) -> None: + """Raises a ValidationError if the given footer is invalid.""" + field_validators = { + 'text': ( + MinLengthValidator( + limit_value=1, + message="Footer text must not be empty." + ), + MaxLengthValidator(limit_value=2048) + ), + 'icon_url': (), + 'proxy_icon_url': () + } + + if not isinstance(footer, Mapping): + raise ValidationError("Embed footer must be a mapping.") + + for field_name, value in footer.items(): + if field_name not in field_validators: + raise ValidationError(f"Unknown embed footer field: {field_name!r}.") + + for validator in field_validators[field_name]: + validator(value) + + +def validate_embed_author(author: Any) -> None: + """Raises a ValidationError if the given author is invalid.""" + field_validators = { + 'name': ( + MinLengthValidator( + limit_value=1, + message="Embed author name must not be empty." + ), + MaxLengthValidator(limit_value=256) + ), + 'url': (), + 'icon_url': (), + 'proxy_icon_url': () + } + + if not isinstance(author, Mapping): + raise ValidationError("Embed author must be a mapping.") + + for field_name, value in author.items(): + if field_name not in field_validators: + raise ValidationError(f"Unknown embed author field: {field_name!r}.") + + for validator in field_validators[field_name]: + validator(value) + + +def validate_embed(embed: Any) -> None: + """ + Validate a JSON document containing an embed as possible to send on Discord. + + This attempts to rebuild the validation used by Discord + as well as possible by checking for various embed limits so we can + ensure that any embed we store here will also be accepted as a + valid embed by the Discord API. + + Using this directly is possible, although not intended - you usually + stick this onto the `validators` keyword argument of model fields. + + Example: + + >>> from django.contrib.postgres import fields as pgfields + >>> from django.db import models + >>> from pydis_site.apps.api.models.utils import validate_embed + >>> class MyMessage(models.Model): + ... embed = pgfields.JSONField( + ... validators=( + ... validate_embed, + ... ) + ... ) + ... # ... + ... + + Args: + embed (Any): + A dictionary describing the contents of this embed. + See the official documentation for a full reference + of accepted keys by this dictionary: + https://discordapp.com/developers/docs/resources/channel#embed-object + + Raises: + ValidationError: + In case the given embed is deemed invalid, a `ValidationError` + is raised which in turn will allow Django to display errors + as appropriate. + """ + all_keys = { + 'title', 'type', 'description', 'url', 'timestamp', + 'color', 'footer', 'image', 'thumbnail', 'video', + 'provider', 'author', 'fields' + } + one_required_of = {'description', 'fields', 'image', 'title', 'video'} + field_validators = { + 'title': ( + MinLengthValidator( + limit_value=1, + message="Embed title must not be empty." + ), + MaxLengthValidator(limit_value=256) + ), + 'description': (MaxLengthValidator(limit_value=2048),), + 'fields': ( + MaxLengthValidator(limit_value=25), + validate_embed_fields + ), + 'footer': (validate_embed_footer,), + 'author': (validate_embed_author,) + } + + if not embed: + raise ValidationError("Tag embed must not be empty.") + + elif not isinstance(embed, Mapping): + raise ValidationError("Tag embed must be a mapping.") + + elif not any(field in embed for field in one_required_of): + raise ValidationError(f"Tag embed must contain one of the fields {one_required_of}.") + + for required_key in one_required_of: + if required_key in embed and not embed[required_key]: + raise ValidationError(f"Key {required_key!r} must not be empty.") + + for field_name, value in embed.items(): + if field_name not in all_keys: + raise ValidationError(f"Unknown field name: {field_name!r}") + + if field_name in field_validators: + for validator in field_validators[field_name]: + validator(value) -- cgit v1.2.3 From 1ab4ccbbbcf38dddace5855ed9521219821b7698 Mon Sep 17 00:00:00 2001 From: Karlis S Date: Thu, 27 Aug 2020 04:48:09 +0000 Subject: Remove unused import from models utils --- pydis_site/apps/api/models/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pydis_site/apps/api/models/utils.py b/pydis_site/apps/api/models/utils.py index 97a79507..107231ba 100644 --- a/pydis_site/apps/api/models/utils.py +++ b/pydis_site/apps/api/models/utils.py @@ -1,5 +1,4 @@ from collections.abc import Mapping -from operator import itemgetter from typing import Any, Dict from django.core.exceptions import ValidationError -- cgit v1.2.3 From fd2909c936efb9ab2285896b297b405c4dd7a1fb Mon Sep 17 00:00:00 2001 From: Karlis S Date: Thu, 27 Aug 2020 05:35:01 +0000 Subject: Move last parts from mixins to utils and delete mixins --- pydis_site/apps/api/models/mixins.py | 31 ------------------------------- pydis_site/apps/api/models/utils.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 31 deletions(-) delete mode 100644 pydis_site/apps/api/models/mixins.py diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py deleted file mode 100644 index 5d75b78b..00000000 --- a/pydis_site/apps/api/models/mixins.py +++ /dev/null @@ -1,31 +0,0 @@ -from operator import itemgetter - -from django.db import models - - -class ModelReprMixin: - """Mixin providing a `__repr__()` to display model class name and initialisation parameters.""" - - def __repr__(self): - """Returns the current model class name and initialisation parameters.""" - attributes = ' '.join( - f'{attribute}={value!r}' - for attribute, value in sorted( - self.__dict__.items(), - key=itemgetter(0) - ) - if not attribute.startswith('_') - ) - return f'<{self.__class__.__name__}({attributes})>' - - -class ModelTimestampMixin(models.Model): - """Mixin providing created_at and updated_at fields.""" - - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - """Metaconfig for the mixin.""" - - abstract = True diff --git a/pydis_site/apps/api/models/utils.py b/pydis_site/apps/api/models/utils.py index 107231ba..692d14f7 100644 --- a/pydis_site/apps/api/models/utils.py +++ b/pydis_site/apps/api/models/utils.py @@ -1,8 +1,10 @@ from collections.abc import Mapping +from operator import itemgetter from typing import Any, Dict from django.core.exceptions import ValidationError from django.core.validators import MaxLengthValidator, MinLengthValidator +from django.db import models def is_bool_validator(value: Any) -> None: @@ -171,3 +173,31 @@ def validate_embed(embed: Any) -> None: if field_name in field_validators: for validator in field_validators[field_name]: validator(value) + + +class ModelReprMixin: + """Mixin providing a `__repr__()` to display model class name and initialisation parameters.""" + + def __repr__(self): + """Returns the current model class name and initialisation parameters.""" + attributes = ' '.join( + f'{attribute}={value!r}' + for attribute, value in sorted( + self.__dict__.items(), + key=itemgetter(0) + ) + if not attribute.startswith('_') + ) + return f'<{self.__class__.__name__}({attributes})>' + + +class ModelTimestampMixin(models.Model): + """Mixin providing created_at and updated_at fields.""" + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + """Metaconfig for the mixin.""" + + abstract = True -- cgit v1.2.3 From 4eeb841724e7603dffc5334a6e4aa7b7b7ced997 Mon Sep 17 00:00:00 2001 From: Karlis S Date: Thu, 27 Aug 2020 05:37:55 +0000 Subject: Fix FilterList model mixins import path --- pydis_site/apps/api/models/bot/filter_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/apps/api/models/bot/filter_list.py b/pydis_site/apps/api/models/bot/filter_list.py index d279e137..cb4acb68 100644 --- a/pydis_site/apps/api/models/bot/filter_list.py +++ b/pydis_site/apps/api/models/bot/filter_list.py @@ -1,6 +1,6 @@ from django.db import models -from pydis_site.apps.api.models.mixins import ModelReprMixin, ModelTimestampMixin +from pydis_site.apps.api.models.utils import ModelReprMixin, ModelTimestampMixin class FilterList(ModelTimestampMixin, ModelReprMixin, models.Model): -- cgit v1.2.3 From feafa0950d9132350e36f58fbae57121363d3277 Mon Sep 17 00:00:00 2001 From: Karlis S Date: Thu, 27 Aug 2020 05:47:47 +0000 Subject: Still move mixins back to its own file --- pydis_site/apps/api/models/bot/filter_list.py | 2 +- pydis_site/apps/api/models/mixins.py | 31 +++++++++++++++++++++++++++ pydis_site/apps/api/models/utils.py | 30 -------------------------- 3 files changed, 32 insertions(+), 31 deletions(-) create mode 100644 pydis_site/apps/api/models/mixins.py diff --git a/pydis_site/apps/api/models/bot/filter_list.py b/pydis_site/apps/api/models/bot/filter_list.py index cb4acb68..d279e137 100644 --- a/pydis_site/apps/api/models/bot/filter_list.py +++ b/pydis_site/apps/api/models/bot/filter_list.py @@ -1,6 +1,6 @@ from django.db import models -from pydis_site.apps.api.models.utils import ModelReprMixin, ModelTimestampMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin, ModelTimestampMixin class FilterList(ModelTimestampMixin, ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py new file mode 100644 index 00000000..5d75b78b --- /dev/null +++ b/pydis_site/apps/api/models/mixins.py @@ -0,0 +1,31 @@ +from operator import itemgetter + +from django.db import models + + +class ModelReprMixin: + """Mixin providing a `__repr__()` to display model class name and initialisation parameters.""" + + def __repr__(self): + """Returns the current model class name and initialisation parameters.""" + attributes = ' '.join( + f'{attribute}={value!r}' + for attribute, value in sorted( + self.__dict__.items(), + key=itemgetter(0) + ) + if not attribute.startswith('_') + ) + return f'<{self.__class__.__name__}({attributes})>' + + +class ModelTimestampMixin(models.Model): + """Mixin providing created_at and updated_at fields.""" + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + """Metaconfig for the mixin.""" + + abstract = True diff --git a/pydis_site/apps/api/models/utils.py b/pydis_site/apps/api/models/utils.py index 692d14f7..107231ba 100644 --- a/pydis_site/apps/api/models/utils.py +++ b/pydis_site/apps/api/models/utils.py @@ -1,10 +1,8 @@ from collections.abc import Mapping -from operator import itemgetter from typing import Any, Dict from django.core.exceptions import ValidationError from django.core.validators import MaxLengthValidator, MinLengthValidator -from django.db import models def is_bool_validator(value: Any) -> None: @@ -173,31 +171,3 @@ def validate_embed(embed: Any) -> None: if field_name in field_validators: for validator in field_validators[field_name]: validator(value) - - -class ModelReprMixin: - """Mixin providing a `__repr__()` to display model class name and initialisation parameters.""" - - def __repr__(self): - """Returns the current model class name and initialisation parameters.""" - attributes = ' '.join( - f'{attribute}={value!r}' - for attribute, value in sorted( - self.__dict__.items(), - key=itemgetter(0) - ) - if not attribute.startswith('_') - ) - return f'<{self.__class__.__name__}({attributes})>' - - -class ModelTimestampMixin(models.Model): - """Mixin providing created_at and updated_at fields.""" - - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - """Metaconfig for the mixin.""" - - abstract = True -- cgit v1.2.3 From e1f75feb74c24b262b276534070e4ffb3bf295b3 Mon Sep 17 00:00:00 2001 From: Karlis S Date: Thu, 27 Aug 2020 05:53:09 +0000 Subject: Fix import paths of mixins in message model --- pydis_site/apps/api/models/bot/message.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pydis_site/apps/api/models/bot/message.py b/pydis_site/apps/api/models/bot/message.py index e929ea25..f6ae55a5 100644 --- a/pydis_site/apps/api/models/bot/message.py +++ b/pydis_site/apps/api/models/bot/message.py @@ -6,7 +6,8 @@ from django.db import models from django.utils import timezone from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.utils import ModelReprMixin, validate_embed +from pydis_site.apps.api.models.mixins import ModelReprMixin +from pydis_site.apps.api.models.utils import validate_embed class Message(ModelReprMixin, models.Model): -- cgit v1.2.3 From ec689ad42d28de55d47f9d3730389ae7e179d565 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 27 Aug 2020 08:57:28 +0300 Subject: Fix embed validator location in migration --- pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py b/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py index e617e1c9..b03c8a18 100644 --- a/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py +++ b/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py @@ -16,6 +16,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='deletedmessage', name='embeds', - field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(validators=[pydis_site.apps.api.models.bot.tag.validate_tag_embed]), blank=True, help_text='Embeds attached to this message.', size=None), + field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(validators=[pydis_site.apps.api.models.utils.validate_embed]), blank=True, help_text='Embeds attached to this message.', size=None), ), ] -- cgit v1.2.3 From b43477faa59970adffc93c58743a4abce53b1547 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 27 Aug 2020 09:01:28 +0300 Subject: Replace bad import on migration --- pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py b/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py index b03c8a18..124c6a57 100644 --- a/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py +++ b/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py @@ -3,7 +3,7 @@ import django.contrib.postgres.fields import django.contrib.postgres.fields.jsonb from django.db import migrations -import pydis_site.apps.api.models.bot.tag +import pydis_site.apps.api.models.utils class Migration(migrations.Migration): -- cgit v1.2.3 From 567f7f0c4a71ace555c9b3123ef50d6ae47756cd Mon Sep 17 00:00:00 2001 From: rohanjnr Date: Fri, 28 Aug 2020 12:58:55 +0530 Subject: Add code to replace restframework_bulk package for bulk create and simplify UserListSerializer `to_internal_value()` function is not longer overriden in UserListSerializer, this is due to explicitly stating the `id` field in UserSerializer as mentioned in the documentation. Override `create()` method in UserListSerializer and override `get_serializer()` method in `UserViewSet` to support bulk creation. --- pydis_site/apps/api/serializers.py | 73 ++++++++------------------------ pydis_site/apps/api/viewsets/bot/user.py | 20 +++++++-- 2 files changed, 35 insertions(+), 58 deletions(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 0589ce77..21c488a8 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -1,15 +1,13 @@ """Converters from Django models to data interchange formats and back.""" from django.db.models.query import QuerySet from rest_framework.serializers import ( + IntegerField, ListSerializer, ModelSerializer, PrimaryKeyRelatedField, ValidationError ) -from rest_framework.settings import api_settings -from rest_framework.utils import html from rest_framework.validators import UniqueTogetherValidator -from rest_framework_bulk import BulkSerializerMixin from .models import ( BotSetting, @@ -271,55 +269,18 @@ class TagSerializer(ModelSerializer): class UserListSerializer(ListSerializer): """List serializer for User model to handle bulk updates.""" - def to_internal_value(self, data: list) -> list: - """ - Overriding `to_internal_value` function with a few changes to support bulk updates. - - ref: https://github.com/miki725/django-rest-framework-bulk/issues/68 + def create(self, validated_data: list) -> list: + """Override create method to optimize django queries.""" + present_users = User.objects.all() + new_users = [] + present_user_ids = [user.id for user in present_users] - List of dicts of native values <- List of dicts of primitive datatypes. - """ - if html.is_html_input(data): - data = html.parse_html_list(data, default=[]) + for user_dict in validated_data: + if user_dict["id"] in present_user_ids: + raise ValidationError({"id": "User already exists."}) + new_users.append(User(**user_dict)) - if not isinstance(data, list): - message = self.error_messages['not_a_list'].format( - input_type=type(data).__name__ - ) - raise ValidationError({ - api_settings.NON_FIELD_ERRORS_KEY: [message] - }, code='not_a_list') - - if not self.allow_empty and len(data) == 0: - message = self.error_messages['empty'] - raise ValidationError({ - api_settings.NON_FIELD_ERRORS_KEY: [message] - }, code='empty') - - ret = [] - errors = [] - - for item in data: - # inserted code - # ----------------- - try: - self.child.instance = self.instance.get(id=item['id']) - except (User.DoesNotExist, AttributeError): - self.child.instance = None - # ----------------- - self.child.initial_data = item - try: - validated = self.child.run_validation(item) - except ValidationError as exc: - errors.append(exc.detail) - else: - ret.append(validated) - errors.append({}) - - if any(errors): - raise ValidationError(errors) - - return ret + return User.objects.bulk_create(new_users) def update(self, instance: QuerySet, validated_data: list) -> list: """ @@ -331,17 +292,19 @@ class UserListSerializer(ListSerializer): data_mapping = {item['id']: item for item in validated_data} updated = [] - for book_id, data in data_mapping.items(): - book = instance_mapping.get(book_id, None) - if book is not None: - updated.append(self.child.update(book, data)) + for user_id, data in data_mapping.items(): + user = instance_mapping.get(user_id, None) + if user: + updated.append(self.child.update(user, data)) return updated -class UserSerializer(BulkSerializerMixin, ModelSerializer): +class UserSerializer(ModelSerializer): """A class providing (de-)serialization of `User` instances.""" + id = IntegerField(min_value=0) + class Meta: """Metadata defined for the Django REST Framework.""" diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index d64ca113..d015fe71 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -3,8 +3,8 @@ from rest_framework.decorators import action from rest_framework.pagination import PageNumberPagination from rest_framework.request import Request from rest_framework.response import Response +from rest_framework.serializers import ModelSerializer from rest_framework.viewsets import ModelViewSet -from rest_framework_bulk import BulkCreateModelMixin from pydis_site.apps.api.models.bot.user import User from pydis_site.apps.api.serializers import UserSerializer @@ -17,7 +17,7 @@ class UserListPagination(PageNumberPagination): page_size_query_param = "page_size" -class UserViewSet(BulkCreateModelMixin, ModelViewSet): +class UserViewSet(ModelViewSet): """ View providing CRUD operations on Discord users through the bot. @@ -142,7 +142,14 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): queryset = User.objects.all() pagination_class = UserListPagination - @action(detail=False, methods=["PATCH"]) + def get_serializer(self, *args, **kwargs) -> ModelSerializer: + """Set Serializer many attribute to True if request body contains a list.""" + if isinstance(kwargs.get('data', {}), list): + kwargs['many'] = True + + return super().get_serializer(*args, **kwargs) + + @action(detail=False, methods=["PATCH"], name='user-bulk-patch') def bulk_patch(self, request: Request) -> Response: """ Update multiple User objects in a single request. @@ -186,6 +193,13 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): filtered_instances = queryset.filter(id__in=object_ids) + if filtered_instances.count() != len(object_ids): + # If all user objects passed in request.body are not present in the database. + resp = { + "Error": "User object not found." + } + return Response(resp, status=status.HTTP_404_NOT_FOUND) + serializer = self.get_serializer( instance=filtered_instances, data=request.data, -- cgit v1.2.3 From 2aa12e7f605f56ed31f14aa1c93bf4ef48323678 Mon Sep 17 00:00:00 2001 From: rohanjnr Date: Fri, 28 Aug 2020 13:02:49 +0530 Subject: Add tests for bulk patch for User Model and additional test for bulk creation of User Models. --- pydis_site/apps/api/tests/test_users.py | 116 ++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py index 4c0f6e27..a54e1189 100644 --- a/pydis_site/apps/api/tests/test_users.py +++ b/pydis_site/apps/api/tests/test_users.py @@ -45,6 +45,13 @@ class CreationTests(APISubdomainTestCase): position=1 ) + cls.user = User.objects.create( + id=11, + name="Name doesn't matter.", + discriminator=1122, + in_guild=True + ) + def test_accepts_valid_data(self): url = reverse('bot:user-list', host='api') data = { @@ -115,6 +122,115 @@ class CreationTests(APISubdomainTestCase): response = self.client.post(url, data=data) self.assertEqual(response.status_code, 400) + def test_returns_400_for_user_recreation(self): + """Return 400 if User is already present in database.""" + url = reverse('bot:user-list', host='api') + data = [{ + 'id': 11, + 'name': 'You saw nothing.', + 'discriminator': 112, + 'in_guild': True + }] + response = self.client.post(url, data=data) + self.assertEqual(response.status_code, 400) + + +class MultiPatchTests(APISubdomainTestCase): + @classmethod + def setUpTestData(cls): + cls.role_developer = Role.objects.create( + id=159, + name="Developer", + colour=2, + permissions=0b01010010101, + position=10, + ) + cls.user_1 = User.objects.create( + id=1, + name="Patch test user 1.", + discriminator=1111, + in_guild=True + ) + cls.user_2 = User.objects.create( + id=2, + name="Patch test user 2.", + discriminator=2222, + in_guild=True + ) + + def test_multiple_users_patch(self): + url = reverse("bot:user-bulk-patch", host="api") + data = [ + { + "id": 1, + "name": "User 1 patched!", + "discriminator": 1010, + "roles": [self.role_developer.id], + "in_guild": False + }, + { + "id": 2, + "name": "User 2 patched!" + } + ] + + response = self.client.patch(url, data=data) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()[0], data[0]) + + user_2 = User.objects.get(id=2) + self.assertEqual(user_2.name, data[1]["name"]) + + def test_returns_400_for_missing_user_id(self): + url = reverse("bot:user-bulk-patch", host="api") + data = [ + { + "name": "I am ghost user!", + "discriminator": 1010, + "roles": [self.role_developer.id], + "in_guild": False + }, + { + "name": "patch me? whats my id?" + } + ] + response = self.client.patch(url, data=data) + self.assertEqual(response.status_code, 400) + + def test_returns_404_for_not_found_user(self): + url = reverse("bot:user-bulk-patch", host="api") + data = [ + { + "id": 1, + "name": "User 1 patched again!!!", + "discriminator": 1010, + "roles": [self.role_developer.id], + "in_guild": False + }, + { + "id": 22503405, + "name": "User unknown not patched!" + } + ] + response = self.client.patch(url, data=data) + self.assertEqual(response.status_code, 404) + + def test_returns_400_for_bad_data(self): + url = reverse("bot:user-bulk-patch", host="api") + data = [ + { + "id": 1, + "in_guild": "Catch me!" + }, + { + "id": 2, + "discriminator": "find me!" + } + ] + + response = self.client.patch(url, data=data) + self.assertEqual(response.status_code, 400) + class UserModelTests(APISubdomainTestCase): @classmethod -- cgit v1.2.3 From bfe32de3ab112de577a154d21f522e472b610f6f Mon Sep 17 00:00:00 2001 From: rohanjnr Date: Fri, 28 Aug 2020 13:03:27 +0530 Subject: Uninstall djangorestframework-bulk dependency --- Pipfile | 1 - Pipfile.lock | 303 ++++++++++++++++++++++++----------------------------------- 2 files changed, 121 insertions(+), 183 deletions(-) diff --git a/Pipfile b/Pipfile index 0f794078..c9769c5d 100644 --- a/Pipfile +++ b/Pipfile @@ -9,7 +9,6 @@ django-environ = "~=0.4.5" django-filter = "~=2.1.0" django-hosts = "~=4.0" djangorestframework = "~=3.11.0" -djangorestframework-bulk = "~=0.2.1" psycopg2-binary = "~=2.8" django-simple-bulma = "~=1.2" whitenoise = "~=5.0" diff --git a/Pipfile.lock b/Pipfile.lock index 02d81d76..2993bd36 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "edc59f1c711954bd22606d68e00f44c21c68a7b3193b20e44a86438e24c0f54b" + "sha256": "9f344f6ff220239656ea524c6b1b6e0db6dfcdbfec50433f7cab3af1eaf4f65f" }, "pipfile-spec": 6, "requires": { @@ -56,11 +56,11 @@ }, "django": { "hashes": [ - "sha256:31a5fbbea5fc71c99e288ec0b2f00302a0a92c44b13ede80b73a6a4d6d205582", - "sha256:5457fc953ec560c5521b41fad9e6734a4668b7ba205832191bbdff40ec61073c" + "sha256:96fbe04e8ba0df289171e7f6970e0ff8b472bf4f909ed9e0e5beccbac7e1dbbe", + "sha256:c22b4cd8e388f8219dc121f091e53a8701f9f5bca9aa132b5254263cab516215" ], "index": "pypi", - "version": "==3.0.8" + "version": "==3.0.9" }, "django-allauth": { "hashes": [ @@ -71,9 +71,10 @@ }, "django-classy-tags": { "hashes": [ - "sha256:ad6a25fc2b58a098f00d86bd5e5dad47922f5ca4e744bc3cccb7b4be5bc35eb1" + "sha256:25eb4f95afee396148683bfb4811b83b3f5729218d73ad0a3399271a6f9fcc49", + "sha256:d59d98bdf96a764dcf7a2929a86439d023b283a9152492811c7e44fc47555bc9" ], - "version": "==1.0.0" + "version": "==2.0.0" }, "django-environ": { "hashes": [ @@ -123,32 +124,26 @@ }, "django-sekizai": { "hashes": [ - "sha256:e2f6e666d4dd9d3ecc27284acb85ef709e198014f5d5af8c6d54ed04c2d684d9" + "sha256:5c5e16845d37ce822fc655ce79ec02715191b3d03330b550997bcb842cf24fdf", + "sha256:e829f09b0d6bf01ee5cde05de1fb3faf2fbc5df66dc4dc280fbaac224ca4336f" ], - "version": "==1.1.0" + "version": "==2.0.0" }, "django-simple-bulma": { "hashes": [ - "sha256:a1462088791af5c65d2ea3b5a517a481dd8afb35b324979cdeefa6f3e6c58d3d", - "sha256:a93daf425353834db96840ca4aa7744c796899243f114e73b8159724ce4573c1" + "sha256:79928fa983151947c635acf65fa5177ca775db98c8d53ddf1c785fe48c727466", + "sha256:e5cff3fc5f0d45558362ab8d0e11f92887c4fc85616f77daa6174940f94b12c7" ], "index": "pypi", - "version": "==1.2.0" + "version": "==1.3.2" }, "djangorestframework": { "hashes": [ - "sha256:05809fc66e1c997fd9a32ea5730d9f4ba28b109b9da71fccfa5ff241201fd0a4", - "sha256:e782087823c47a26826ee5b6fa0c542968219263fb3976ec3c31edab23a4001f" + "sha256:6dd02d5a4bd2516fb93f80360673bf540c3b6641fec8766b1da2870a5aa00b32", + "sha256:8b1ac62c581dbc5799b03e535854b92fc4053ecfe74bad3f9c05782063d4196b" ], "index": "pypi", - "version": "==3.11.0" - }, - "djangorestframework-bulk": { - "hashes": [ - "sha256:39230d8379acebd86d313df6c9150cafecb636eae1d097c30a26389ab9fee5b1" - ], - "index": "pypi", - "version": "==0.2.1" + "version": "==3.11.1" }, "idna": { "hashes": [ @@ -158,31 +153,23 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, - "importlib-metadata": { - "hashes": [ - "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83", - "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070" - ], - "markers": "python_version < '3.8'", - "version": "==1.7.0" - }, "libsass": { "hashes": [ - "sha256:107c409524c6a4ed14410fa9dafa9ee59c6bd3ecae75d73af749ab2b75685726", - "sha256:3bc0d68778b30b5fa83199e18795314f64b26ca5871e026343e63934f616f7f7", - "sha256:5c8ff562b233734fbc72b23bb862cc6a6f70b1e9bf85a58422aa75108b94783b", - "sha256:74f6fb8da58179b5d86586bc045c16d93d55074bc7bb48b6354a4da7ac9f9dfd", - "sha256:7555d9b24e79943cfafac44dbb4ca7e62105c038de7c6b999838c9ff7b88645d", - "sha256:794f4f4661667263e7feafe5cc866e3746c7c8a9192b2aa9afffdadcbc91c687", - "sha256:8cf72552b39e78a1852132e16b706406bc76029fe3001583284ece8d8752a60a", - "sha256:98f6dee9850b29e62977a963e3beb3cfeb98b128a267d59d2c3d675e298c8d57", - "sha256:a43f3830d83ad9a7f5013c05ce239ca71744d0780dad906587302ac5257bce60", - "sha256:b077261a04ba1c213e932943208471972c5230222acb7fa97373e55a40872cbb", - "sha256:b7452f1df274b166dc22ee2e9154c4adca619bcbbdf8041a7aa05f372a1dacbc", - "sha256:e6a547c0aa731dcb4ed71f198e814bee0400ce04d553f3f12a53bc3a17f2a481", - "sha256:fd19c8f73f70ffc6cbcca8139da08ea9a71fc48e7dfc4bb236ad88ab2d6558f1" - ], - "version": "==0.20.0" + "sha256:1521d2a8d4b397c6ec90640a1f6b5529077035efc48ef1c2e53095544e713d1b", + "sha256:1b2d415bbf6fa7da33ef46e549db1418498267b459978eff8357e5e823962d35", + "sha256:25ebc2085f5eee574761ccc8d9cd29a9b436fc970546d5ef08c6fa41eb57dff1", + "sha256:2ae806427b28bc1bb7cb0258666d854fcf92ba52a04656b0b17ba5e190fb48a9", + "sha256:4a246e4b88fd279abef8b669206228c92534d96ddcd0770d7012088c408dff23", + "sha256:553e5096414a8d4fb48d0a48f5a038d3411abe254d79deac5e008516c019e63a", + "sha256:697f0f9fa8a1367ca9ec6869437cb235b1c537fc8519983d1d890178614a8903", + "sha256:a8fd4af9f853e8bf42b1425c5e48dd90b504fa2e70d7dac5ac80b8c0a5a5fe85", + "sha256:c9411fec76f480ffbacc97d8188322e02a5abca6fc78e70b86a2a2b421eae8a2", + "sha256:daa98a51086d92aa7e9c8871cf1a8258124b90e2abf4697852a3dca619838618", + "sha256:e0e60836eccbf2d9e24ec978a805cd6642fa92515fbd95e3493fee276af76f8a", + "sha256:e64ae2587f1a683e831409aad03ba547c245ef997e1329fffadf7a866d2510b8", + "sha256:f6852828e9e104d2ce0358b73c550d26dd86cc3a69439438c3b618811b9584f5" + ], + "version": "==0.20.1" }, "markdown": { "hashes": [ @@ -210,32 +197,34 @@ }, "pillow": { "hashes": [ - "sha256:0295442429645fa16d05bd567ef5cff178482439c9aad0411d3f0ce9b88b3a6f", - "sha256:06aba4169e78c439d528fdeb34762c3b61a70813527a2c57f0540541e9f433a8", + "sha256:1ca594126d3c4def54babee699c055a913efb01e106c309fa6b04405d474d5ae", + "sha256:a060cf8aa332052df2158e5a119303965be92c3da6f2d93b6878f0ebca80b2f6", + "sha256:9ad7f865eebde135d526bb3163d0b23ffff365cf87e767c649550964ad72785d", + "sha256:9c87ef410a58dd54b92424ffd7e28fd2ec65d2f7fc02b76f5e9b2067e355ebf6", + "sha256:edf31f1150778abd4322444c393ab9c7bd2af271dd4dafb4208fb613b1f3cdc9", + "sha256:ec29604081f10f16a7aea809ad42e27764188fc258b02259a03a8ff7ded3808d", + "sha256:8dad18b69f710bf3a001d2bf3afab7c432785d94fcf819c16b5207b1cfd17d38", + "sha256:e901964262a56d9ea3c2693df68bc9860b8bdda2b04768821e4c44ae797de117", + "sha256:6edb5446f44d901e8683ffb25ebdfc26988ee813da3bf91e12252b57ac163727", + "sha256:d08b23fdb388c0715990cbc06866db554e1822c4bdcf6d4166cf30ac82df8c41", "sha256:09d7f9e64289cb40c2c8d7ad674b2ed6105f55dc3b09aa8e4918e20a0311e7ad", + "sha256:c92302a33138409e8f1ad16731568c55c9053eee71bb05b6b744067e1b62380f", + "sha256:d350f0f2c2421e65fbc62690f26b59b0bcda1b614beb318c81e38647e0f673a1", + "sha256:06aba4169e78c439d528fdeb34762c3b61a70813527a2c57f0540541e9f433a8", + "sha256:612cfda94e9c8346f239bf1a4b082fdd5c8143cf82d685ba2dba76e7adeeb233", "sha256:0a80dd307a5d8440b0a08bd7b81617e04d870e40a3e46a32d9c246e54705e86f", - "sha256:1ca594126d3c4def54babee699c055a913efb01e106c309fa6b04405d474d5ae", - "sha256:25930fadde8019f374400f7986e8404c8b781ce519da27792cbe46eabec00c4d", "sha256:431b15cffbf949e89df2f7b48528be18b78bfa5177cb3036284a5508159492b5", - "sha256:52125833b070791fcb5710fabc640fc1df07d087fc0c0f02d3661f76c23c5b8b", - "sha256:5e51ee2b8114def244384eda1c82b10e307ad9778dac5c83fb0943775a653cd8", - "sha256:612cfda94e9c8346f239bf1a4b082fdd5c8143cf82d685ba2dba76e7adeeb233", - "sha256:6d7741e65835716ceea0fd13a7d0192961212fd59e741a46bbed7a473c634ed6", - "sha256:6edb5446f44d901e8683ffb25ebdfc26988ee813da3bf91e12252b57ac163727", - "sha256:725aa6cfc66ce2857d585f06e9519a1cc0ef6d13f186ff3447ab6dff0a09bc7f", - "sha256:8dad18b69f710bf3a001d2bf3afab7c432785d94fcf819c16b5207b1cfd17d38", - "sha256:94cf49723928eb6070a892cb39d6c156f7b5a2db4e8971cb958f7b6b104fb4c4", "sha256:97f9e7953a77d5a70f49b9a48da7776dc51e9b738151b22dacf101641594a626", - "sha256:9ad7f865eebde135d526bb3163d0b23ffff365cf87e767c649550964ad72785d", - "sha256:a060cf8aa332052df2158e5a119303965be92c3da6f2d93b6878f0ebca80b2f6", + "sha256:725aa6cfc66ce2857d585f06e9519a1cc0ef6d13f186ff3447ab6dff0a09bc7f", "sha256:c79f9c5fb846285f943aafeafda3358992d64f0ef58566e23484132ecd8d7d63", - "sha256:c92302a33138409e8f1ad16731568c55c9053eee71bb05b6b744067e1b62380f", - "sha256:d08b23fdb388c0715990cbc06866db554e1822c4bdcf6d4166cf30ac82df8c41", - "sha256:d350f0f2c2421e65fbc62690f26b59b0bcda1b614beb318c81e38647e0f673a1", - "sha256:ec29604081f10f16a7aea809ad42e27764188fc258b02259a03a8ff7ded3808d", - "sha256:edf31f1150778abd4322444c393ab9c7bd2af271dd4dafb4208fb613b1f3cdc9", + "sha256:94cf49723928eb6070a892cb39d6c156f7b5a2db4e8971cb958f7b6b104fb4c4", + "sha256:25930fadde8019f374400f7986e8404c8b781ce519da27792cbe46eabec00c4d", + "sha256:6d7741e65835716ceea0fd13a7d0192961212fd59e741a46bbed7a473c634ed6", + "sha256:5e51ee2b8114def244384eda1c82b10e307ad9778dac5c83fb0943775a653cd8", "sha256:f7e30c27477dffc3e85c2463b3e649f751789e0f6c8456099eea7ddd53be4a8a", - "sha256:ffe538682dc19cc542ae7c3e504fdf54ca7f86fb8a135e59dd6bc8627eae6cce" + "sha256:ffe538682dc19cc542ae7c3e504fdf54ca7f86fb8a135e59dd6bc8627eae6cce", + "sha256:52125833b070791fcb5710fabc640fc1df07d087fc0c0f02d3661f76c23c5b8b", + "sha256:0295442429645fa16d05bd567ef5cff178482439c9aad0411d3f0ce9b88b3a6f" ], "markers": "python_version >= '3.5'", "version": "==7.2.0" @@ -360,11 +349,11 @@ }, "sentry-sdk": { "hashes": [ - "sha256:2f023ff348359ec5f0b73a840e8b08e6a8d3b2613a98c57d11c222ef43879237", - "sha256:380a280cfc7c4ade5912294e6d9aa71ce776b5fca60a3782e9331b0bcd2866bf" + "sha256:09cbc253c827a88064c5ed548d24fb4294568bfe9b1816a857fa5a423d4ce762", + "sha256:1d654ac57be9967dae67545fb759f6e7594de07f487c21a276e6466dd52e83f1" ], "index": "pypi", - "version": "==0.16.1" + "version": "==0.17.0" }, "six": { "hashes": [ @@ -392,11 +381,11 @@ }, "urllib3": { "hashes": [ - "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", - "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" + "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", + "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.25.9" + "version": "==1.25.10" }, "webencodings": { "hashes": [ @@ -407,11 +396,11 @@ }, "whitenoise": { "hashes": [ - "sha256:60154b976a13901414a25b0273a841145f77eb34a141f9ae032a0ace3e4d5b27", - "sha256:6dd26bfda3af29177d8ab7333a0c7b7642eb615ce83764f4d15a9aecda3201c4" + "sha256:05ce0be39ad85740a78750c86a93485c40f08ad8c62a6006de0233765996e5c7", + "sha256:05d00198c777028d72d8b0bbd234db605ef6d60e9410125124002518a48e515d" ], "index": "pypi", - "version": "==5.1.0" + "version": "==5.2.0" }, "wiki": { "hashes": [ @@ -420,14 +409,6 @@ ], "index": "pypi", "version": "==0.6" - }, - "zipp": { - "hashes": [ - "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", - "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" - ], - "markers": "python_version >= '3.6'", - "version": "==3.1.0" } }, "develop": { @@ -440,11 +421,11 @@ }, "attrs": { "hashes": [ - "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", - "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a", + "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==19.3.0" + "version": "==20.1.0" }, "bandit": { "hashes": [ @@ -455,51 +436,51 @@ }, "cfgv": { "hashes": [ - "sha256:1ccf53320421aeeb915275a196e23b3b8ae87dea8ac6698b1638001d4a486d53", - "sha256:c8e8f552ffcc6194f4e18dd4f68d9aef0c0d58ae7e7be8c82bee3c5e9edfa513" + "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d", + "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1" ], "markers": "python_full_version >= '3.6.1'", - "version": "==3.1.0" + "version": "==3.2.0" }, "coverage": { "hashes": [ - "sha256:0fc4e0d91350d6f43ef6a61f64a48e917637e1dcfcba4b4b7d543c628ef82c2d", - "sha256:10f2a618a6e75adf64329f828a6a5b40244c1c50f5ef4ce4109e904e69c71bd2", - "sha256:12eaccd86d9a373aea59869bc9cfa0ab6ba8b1477752110cb4c10d165474f703", - "sha256:1874bdc943654ba46d28f179c1846f5710eda3aeb265ff029e0ac2b52daae404", - "sha256:1dcebae667b73fd4aa69237e6afb39abc2f27520f2358590c1b13dd90e32abe7", - "sha256:1e58fca3d9ec1a423f1b7f2aa34af4f733cbfa9020c8fe39ca451b6071237405", - "sha256:214eb2110217f2636a9329bc766507ab71a3a06a8ea30cdeebb47c24dce5972d", - "sha256:25fe74b5b2f1b4abb11e103bb7984daca8f8292683957d0738cd692f6a7cc64c", - "sha256:32ecee61a43be509b91a526819717d5e5650e009a8d5eda8631a59c721d5f3b6", - "sha256:3740b796015b889e46c260ff18b84683fa2e30f0f75a171fb10d2bf9fb91fc70", - "sha256:3b2c34690f613525672697910894b60d15800ac7e779fbd0fccf532486c1ba40", - "sha256:41d88736c42f4a22c494c32cc48a05828236e37c991bd9760f8923415e3169e4", - "sha256:42fa45a29f1059eda4d3c7b509589cc0343cd6bbf083d6118216830cd1a51613", - "sha256:4bb385a747e6ae8a65290b3df60d6c8a692a5599dc66c9fa3520e667886f2e10", - "sha256:509294f3e76d3f26b35083973fbc952e01e1727656d979b11182f273f08aa80b", - "sha256:5c74c5b6045969b07c9fb36b665c9cac84d6c174a809fc1b21bdc06c7836d9a0", - "sha256:60a3d36297b65c7f78329b80120f72947140f45b5c7a017ea730f9112b40f2ec", - "sha256:6f91b4492c5cde83bfe462f5b2b997cdf96a138f7c58b1140f05de5751623cf1", - "sha256:7403675df5e27745571aba1c957c7da2dacb537c21e14007ec3a417bf31f7f3d", - "sha256:87bdc8135b8ee739840eee19b184804e5d57f518578ffc797f5afa2c3c297913", - "sha256:8a3decd12e7934d0254939e2bf434bf04a5890c5bf91a982685021786a08087e", - "sha256:9702e2cb1c6dec01fb8e1a64c015817c0800a6eca287552c47a5ee0ebddccf62", - "sha256:a4d511012beb967a39580ba7d2549edf1e6865a33e5fe51e4dce550522b3ac0e", - "sha256:bbb387811f7a18bdc61a2ea3d102be0c7e239b0db9c83be7bfa50f095db5b92a", - "sha256:bfcc811883699ed49afc58b1ed9f80428a18eb9166422bce3c31a53dba00fd1d", - "sha256:c32aa13cc3fe86b0f744dfe35a7f879ee33ac0a560684fef0f3e1580352b818f", - "sha256:ca63dae130a2e788f2b249200f01d7fa240f24da0596501d387a50e57aa7075e", - "sha256:d54d7ea74cc00482a2410d63bf10aa34ebe1c49ac50779652106c867f9986d6b", - "sha256:d67599521dff98ec8c34cd9652cbcfe16ed076a2209625fca9dc7419b6370e5c", - "sha256:d82db1b9a92cb5c67661ca6616bdca6ff931deceebb98eecbd328812dab52032", - "sha256:d9ad0a988ae20face62520785ec3595a5e64f35a21762a57d115dae0b8fb894a", - "sha256:ebf2431b2d457ae5217f3a1179533c456f3272ded16f8ed0b32961a6d90e38ee", - "sha256:ed9a21502e9223f563e071759f769c3d6a2e1ba5328c31e86830368e8d78bc9c", - "sha256:f50632ef2d749f541ca8e6c07c9928a37f87505ce3a9f20c8446ad310f1aa87b" + "sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb", + "sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3", + "sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716", + "sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034", + "sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3", + "sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8", + "sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0", + "sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f", + "sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4", + "sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962", + "sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d", + "sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b", + "sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4", + "sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3", + "sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258", + "sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59", + "sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01", + "sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd", + "sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b", + "sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d", + "sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89", + "sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd", + "sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b", + "sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d", + "sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46", + "sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546", + "sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082", + "sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b", + "sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4", + "sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8", + "sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811", + "sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd", + "sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651", + "sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0" ], "index": "pypi", - "version": "==5.2" + "version": "==5.2.1" }, "distlib": { "hashes": [ @@ -610,19 +591,11 @@ }, "identify": { "hashes": [ - "sha256:06b4373546ae55eaaefdac54f006951dbd968fe2912846c00e565b09cfaed101", - "sha256:5519601b70c831011fb425ffd214101df7639ba3980f24dc283f7675b19127b3" + "sha256:9f5fcf22b665eaece583bd395b103c2769772a0f646ffabb5b1f155901b07de2", + "sha256:b1aa2e05863dc80242610d46a7b49105e2eafe00ef0c8ff311c1828680760c76" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.4.24" - }, - "importlib-metadata": { - "hashes": [ - "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83", - "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070" - ], - "markers": "python_version < '3.8'", - "version": "==1.7.0" + "version": "==1.4.29" }, "mccabe": { "hashes": [ @@ -634,9 +607,10 @@ }, "nodeenv": { "hashes": [ - "sha256:4b0b77afa3ba9b54f4b6396e60b0c83f59eaeb2d63dc3cc7a70f7f4af96c82bc" + "sha256:5304d424c529c997bc888453aeaa6362d242b6b4631e90f3d4bf1b290f1c84a9", + "sha256:ab45090ae383b716c4ef89e690c41ff8c2b257b85b309f01f3654df3d084bd7c" ], - "version": "==1.4.0" + "version": "==1.5.0" }, "pbr": { "hashes": [ @@ -655,11 +629,11 @@ }, "pre-commit": { "hashes": [ - "sha256:1657663fdd63a321a4a739915d7d03baedd555b25054449090f97bb0cb30a915", - "sha256:e8b1315c585052e729ab7e99dcca5698266bedce9067d21dc909c23e3ceed626" + "sha256:810aef2a2ba4f31eed1941fc270e72696a1ad5590b9751839c90807d0fff6b9a", + "sha256:c54fd3e574565fe128ecc5e7d2f91279772ddb03f8729645fa812fe809084a70" ], "index": "pypi", - "version": "==2.6.0" + "version": "==2.7.1" }, "pycodestyle": { "hashes": [ @@ -671,11 +645,11 @@ }, "pydocstyle": { "hashes": [ - "sha256:da7831660b7355307b32778c4a0dbfb137d89254ef31a2b2978f50fc0b4d7586", - "sha256:f4f5d210610c2d153fae39093d44224c17429e2ad7da12a8b419aba5c2f614b5" + "sha256:08374b9d4d2b7164bae50b71bb24eb0d74a56b309029d5d502264092fa7db0c3", + "sha256:4ca3c7736d36f92bb215dd74ef84ac3d6c146edd795c7afc5154c10f1eb1f65a" ], "markers": "python_version >= '3.5'", - "version": "==5.0.2" + "version": "==5.1.0" }, "pyflakes": { "hashes": [ @@ -740,56 +714,21 @@ ], "version": "==0.10.1" }, - "typed-ast": { - "hashes": [ - "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", - "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", - "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", - "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", - "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", - "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", - "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", - "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", - "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", - "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", - "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", - "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", - "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", - "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", - "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", - "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", - "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", - "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", - "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", - "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", - "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" - ], - "markers": "python_version < '3.8'", - "version": "==1.4.1" - }, "unittest-xml-reporting": { "hashes": [ - "sha256:74eaf7739a7957a74f52b8187c5616f61157372189bef0a32ba5c30bbc00e58a", - "sha256:e09b8ae70cce9904cdd331f53bf929150962869a5324ab7ff3dd6c8b87e01f7d" + "sha256:7bf515ea8cb244255a25100cd29db611a73f8d3d0aaf672ed3266307e14cc1ca", + "sha256:984cebba69e889401bfe3adb9088ca376b3a1f923f0590d005126c1bffd1a695" ], "index": "pypi", - "version": "==3.0.2" + "version": "==3.0.4" }, "virtualenv": { "hashes": [ - "sha256:26cdd725a57fef4c7c22060dba4647ebd8ca377e30d1c1cf547b30a0b79c43b4", - "sha256:c51f1ba727d1614ce8fd62457748b469fbedfdab2c7e5dd480c9ae3fbe1233f1" + "sha256:43add625c53c596d38f971a465553f6318decc39d98512bc100fa1b1e839c8dc", + "sha256:e0305af10299a7fb0d69393d8f04cb2965dda9351140d11ac8db4e5e3970451b" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.0.27" - }, - "zipp": { - "hashes": [ - "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", - "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" - ], - "markers": "python_version >= '3.6'", - "version": "==3.1.0" + "version": "==20.0.31" } } } -- cgit v1.2.3 From b3d4888953ebe110ef04388f10d6dba30f460df9 Mon Sep 17 00:00:00 2001 From: rohanjnr Date: Fri, 28 Aug 2020 21:03:53 +0530 Subject: re-lcok pipfile --- Pipfile.lock | 103 ++++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 81 insertions(+), 22 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 2993bd36..d9348899 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -153,6 +153,14 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, + "importlib-metadata": { + "hashes": [ + "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83", + "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070" + ], + "markers": "python_version < '3.8'", + "version": "==1.7.0" + }, "libsass": { "hashes": [ "sha256:1521d2a8d4b397c6ec90640a1f6b5529077035efc48ef1c2e53095544e713d1b", @@ -197,34 +205,34 @@ }, "pillow": { "hashes": [ - "sha256:1ca594126d3c4def54babee699c055a913efb01e106c309fa6b04405d474d5ae", - "sha256:a060cf8aa332052df2158e5a119303965be92c3da6f2d93b6878f0ebca80b2f6", - "sha256:9ad7f865eebde135d526bb3163d0b23ffff365cf87e767c649550964ad72785d", - "sha256:9c87ef410a58dd54b92424ffd7e28fd2ec65d2f7fc02b76f5e9b2067e355ebf6", - "sha256:edf31f1150778abd4322444c393ab9c7bd2af271dd4dafb4208fb613b1f3cdc9", - "sha256:ec29604081f10f16a7aea809ad42e27764188fc258b02259a03a8ff7ded3808d", - "sha256:8dad18b69f710bf3a001d2bf3afab7c432785d94fcf819c16b5207b1cfd17d38", - "sha256:e901964262a56d9ea3c2693df68bc9860b8bdda2b04768821e4c44ae797de117", - "sha256:6edb5446f44d901e8683ffb25ebdfc26988ee813da3bf91e12252b57ac163727", - "sha256:d08b23fdb388c0715990cbc06866db554e1822c4bdcf6d4166cf30ac82df8c41", - "sha256:09d7f9e64289cb40c2c8d7ad674b2ed6105f55dc3b09aa8e4918e20a0311e7ad", - "sha256:c92302a33138409e8f1ad16731568c55c9053eee71bb05b6b744067e1b62380f", - "sha256:d350f0f2c2421e65fbc62690f26b59b0bcda1b614beb318c81e38647e0f673a1", + "sha256:0295442429645fa16d05bd567ef5cff178482439c9aad0411d3f0ce9b88b3a6f", "sha256:06aba4169e78c439d528fdeb34762c3b61a70813527a2c57f0540541e9f433a8", - "sha256:612cfda94e9c8346f239bf1a4b082fdd5c8143cf82d685ba2dba76e7adeeb233", + "sha256:09d7f9e64289cb40c2c8d7ad674b2ed6105f55dc3b09aa8e4918e20a0311e7ad", "sha256:0a80dd307a5d8440b0a08bd7b81617e04d870e40a3e46a32d9c246e54705e86f", + "sha256:1ca594126d3c4def54babee699c055a913efb01e106c309fa6b04405d474d5ae", + "sha256:25930fadde8019f374400f7986e8404c8b781ce519da27792cbe46eabec00c4d", "sha256:431b15cffbf949e89df2f7b48528be18b78bfa5177cb3036284a5508159492b5", - "sha256:97f9e7953a77d5a70f49b9a48da7776dc51e9b738151b22dacf101641594a626", + "sha256:52125833b070791fcb5710fabc640fc1df07d087fc0c0f02d3661f76c23c5b8b", + "sha256:5e51ee2b8114def244384eda1c82b10e307ad9778dac5c83fb0943775a653cd8", + "sha256:612cfda94e9c8346f239bf1a4b082fdd5c8143cf82d685ba2dba76e7adeeb233", + "sha256:6d7741e65835716ceea0fd13a7d0192961212fd59e741a46bbed7a473c634ed6", + "sha256:6edb5446f44d901e8683ffb25ebdfc26988ee813da3bf91e12252b57ac163727", "sha256:725aa6cfc66ce2857d585f06e9519a1cc0ef6d13f186ff3447ab6dff0a09bc7f", - "sha256:c79f9c5fb846285f943aafeafda3358992d64f0ef58566e23484132ecd8d7d63", + "sha256:8dad18b69f710bf3a001d2bf3afab7c432785d94fcf819c16b5207b1cfd17d38", "sha256:94cf49723928eb6070a892cb39d6c156f7b5a2db4e8971cb958f7b6b104fb4c4", - "sha256:25930fadde8019f374400f7986e8404c8b781ce519da27792cbe46eabec00c4d", - "sha256:6d7741e65835716ceea0fd13a7d0192961212fd59e741a46bbed7a473c634ed6", - "sha256:5e51ee2b8114def244384eda1c82b10e307ad9778dac5c83fb0943775a653cd8", + "sha256:97f9e7953a77d5a70f49b9a48da7776dc51e9b738151b22dacf101641594a626", + "sha256:9ad7f865eebde135d526bb3163d0b23ffff365cf87e767c649550964ad72785d", + "sha256:9c87ef410a58dd54b92424ffd7e28fd2ec65d2f7fc02b76f5e9b2067e355ebf6", + "sha256:a060cf8aa332052df2158e5a119303965be92c3da6f2d93b6878f0ebca80b2f6", + "sha256:c79f9c5fb846285f943aafeafda3358992d64f0ef58566e23484132ecd8d7d63", + "sha256:c92302a33138409e8f1ad16731568c55c9053eee71bb05b6b744067e1b62380f", + "sha256:d08b23fdb388c0715990cbc06866db554e1822c4bdcf6d4166cf30ac82df8c41", + "sha256:d350f0f2c2421e65fbc62690f26b59b0bcda1b614beb318c81e38647e0f673a1", + "sha256:e901964262a56d9ea3c2693df68bc9860b8bdda2b04768821e4c44ae797de117", + "sha256:ec29604081f10f16a7aea809ad42e27764188fc258b02259a03a8ff7ded3808d", + "sha256:edf31f1150778abd4322444c393ab9c7bd2af271dd4dafb4208fb613b1f3cdc9", "sha256:f7e30c27477dffc3e85c2463b3e649f751789e0f6c8456099eea7ddd53be4a8a", - "sha256:ffe538682dc19cc542ae7c3e504fdf54ca7f86fb8a135e59dd6bc8627eae6cce", - "sha256:52125833b070791fcb5710fabc640fc1df07d087fc0c0f02d3661f76c23c5b8b", - "sha256:0295442429645fa16d05bd567ef5cff178482439c9aad0411d3f0ce9b88b3a6f" + "sha256:ffe538682dc19cc542ae7c3e504fdf54ca7f86fb8a135e59dd6bc8627eae6cce" ], "markers": "python_version >= '3.5'", "version": "==7.2.0" @@ -409,6 +417,14 @@ ], "index": "pypi", "version": "==0.6" + }, + "zipp": { + "hashes": [ + "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", + "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" + ], + "markers": "python_version >= '3.6'", + "version": "==3.1.0" } }, "develop": { @@ -597,6 +613,14 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.4.29" }, + "importlib-metadata": { + "hashes": [ + "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83", + "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070" + ], + "markers": "python_version < '3.8'", + "version": "==1.7.0" + }, "mccabe": { "hashes": [ "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", @@ -714,6 +738,33 @@ ], "version": "==0.10.1" }, + "typed-ast": { + "hashes": [ + "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", + "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", + "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", + "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", + "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", + "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", + "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", + "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", + "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", + "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", + "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", + "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", + "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", + "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", + "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", + "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", + "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", + "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", + "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", + "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", + "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" + ], + "markers": "python_version < '3.8'", + "version": "==1.4.1" + }, "unittest-xml-reporting": { "hashes": [ "sha256:7bf515ea8cb244255a25100cd29db611a73f8d3d0aaf672ed3266307e14cc1ca", @@ -729,6 +780,14 @@ ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.0.31" + }, + "zipp": { + "hashes": [ + "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", + "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" + ], + "markers": "python_version >= '3.6'", + "version": "==3.1.0" } } } -- cgit v1.2.3 From 982ef7515f9a9f66098c8047be1792fae9f05016 Mon Sep 17 00:00:00 2001 From: rohanjnr Date: Fri, 28 Aug 2020 21:40:05 +0530 Subject: remove redundant if clause in update() method in UserListSeriazlier. --- pydis_site/apps/api/serializers.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 21c488a8..0915658a 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -293,9 +293,8 @@ class UserListSerializer(ListSerializer): updated = [] for user_id, data in data_mapping.items(): - user = instance_mapping.get(user_id, None) - if user: - updated.append(self.child.update(user, data)) + user = instance_mapping.get(user_id) + updated.append(self.child.update(user, data)) return updated -- cgit v1.2.3 From 32b76b192d72f4235a64b8024131988b4e4c0c36 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 29 Aug 2020 08:55:57 +0300 Subject: Simplify non-random off-topic names selection --- .../api/viewsets/bot/off_topic_channel_name.py | 39 ++++++++++------------ 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py index 9af69ae4..29978015 100644 --- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py @@ -1,3 +1,4 @@ +from django.db.models import Case, When, Value from django.db.models.query import QuerySet from django.http.request import HttpRequest from django.shortcuts import get_object_or_404 @@ -108,28 +109,22 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): 'random_items': ["Must be a positive integer."] }) - queryset = self.get_queryset().order_by('?').exclude(used=True)[:random_count] - self.get_queryset().filter( - name__in=(query.name for query in queryset) - ).update(used=True) - - # When the client requests more channel names than are available, - # we reset all names to used=False and start a new round of names. - if len(queryset) < random_count: - # Figure out how many additional names we need, and don't fetch duplicate names. - names_needed = random_count - len(queryset) - other_names = self.get_queryset().order_by('?').exclude( - name__in=(query.name for query in queryset) - )[:names_needed] - - # Reset the `used` field to False for all names except the ones we just used. - self.get_queryset().exclude(name__in=( - query.name for query in other_names) - ).update(used=False) - - # Join original queryset (that had missing names) - # and extension with these missing names. - queryset = list(queryset) + list(other_names) + queryset = self.get_queryset().order_by('used', '?')[:random_count] + + # When any name is used in our listing then this means we reached end of round + # and we need to reset all other names `used` to False + if any(offtopic_name.used for offtopic_name in queryset): + self.get_queryset().update( + used=Case( # These names that we just got have to be excluded from updating to False + When(name__in=(offtopic_name.name for offtopic_name in queryset), then=Value(True)), + default=Value(False) + ) + ) + else: + # Otherwise mark selected names `used` to True + self.get_queryset().filter( + name__in=(offtopic_name.name for offtopic_name in queryset) + ).update(used=True) serialized = self.serializer_class(queryset, many=True) return Response(serialized.data) -- cgit v1.2.3 From d5945b3d82124214d1bbbf8e156f3516fd3a70cc Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 29 Aug 2020 09:06:11 +0300 Subject: Fix off-topic names test docstrings mood --- .../apps/api/tests/test_off_topic_channel_names.py | 30 +++++++++++----------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/pydis_site/apps/api/tests/test_off_topic_channel_names.py b/pydis_site/apps/api/tests/test_off_topic_channel_names.py index cac9405a..3ab8b22d 100644 --- a/pydis_site/apps/api/tests/test_off_topic_channel_names.py +++ b/pydis_site/apps/api/tests/test_off_topic_channel_names.py @@ -10,14 +10,14 @@ class UnauthenticatedTests(APISubdomainTestCase): self.client.force_authenticate(user=None) def test_cannot_read_off_topic_channel_name_list(self): - """Test does this return 401 response code when not authenticated.""" + """Return a 401 response when not authenticated.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(url) self.assertEqual(response.status_code, 401) def test_cannot_read_off_topic_channel_name_list_with_random_item_param(self): - """Test does this give 401 code when `random_items` provided and not authenticated.""" + """Return a 401 response when `random_items` provided and not authenticated.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=no') @@ -26,7 +26,7 @@ class UnauthenticatedTests(APISubdomainTestCase): class EmptyDatabaseTests(APISubdomainTestCase): def test_returns_empty_object(self): - """Test does this return empty list when no names in database.""" + """Return empty list when no names in database.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(url) @@ -34,7 +34,7 @@ class EmptyDatabaseTests(APISubdomainTestCase): self.assertEqual(response.json(), []) def test_returns_empty_list_with_get_all_param(self): - """Test does this return empty list when no names and `random_items` param provided.""" + """Return empty list when no names and `random_items` param provided.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=5') @@ -42,7 +42,7 @@ class EmptyDatabaseTests(APISubdomainTestCase): self.assertEqual(response.json(), []) def test_returns_400_for_bad_random_items_param(self): - """Test does this return error message when passing not integer as `random_items`.""" + """Return error message when passing not integer as `random_items`.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=totally-a-valid-integer') @@ -52,7 +52,7 @@ class EmptyDatabaseTests(APISubdomainTestCase): }) def test_returns_400_for_negative_random_items_param(self): - """Test does this return error message when passing negative int as `random_items`.""" + """Return error message when passing negative int as `random_items`.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=-5') @@ -69,7 +69,7 @@ class ListTests(APISubdomainTestCase): cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk', used=True) def test_returns_name_in_list(self): - """Test does this return all off-topic channel names.""" + """Return all off-topic channel names.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(url) @@ -83,7 +83,7 @@ class ListTests(APISubdomainTestCase): ) def test_returns_single_item_with_random_items_param_set_to_1(self): - """Test does this return not-used name instead used.""" + """Return not-used name instead used.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=1') @@ -92,7 +92,7 @@ class ListTests(APISubdomainTestCase): self.assertEqual(response.json(), [self.test_name.name]) def test_running_out_of_names_with_random_parameter(self): - """Test does this reset names `used` parameter to `False` when running out of names.""" + """Reset names `used` parameter to `False` when running out of names.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=2') @@ -110,7 +110,7 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response.status_code, 201) def test_returns_201_for_unicode_chars(self): - """Test does this accept all valid characters.""" + """Accept all valid characters.""" url = reverse('bot:offtopicchannelname-list', host='api') names = ( '๐– ๐–ก๐–ข๐–ฃ๐–ค๐–ฅ๐–ฆ๐–ง๐–จ๐–ฉ๐–ช๐–ซ๐–ฌ๐–ญ๐–ฎ๐–ฏ๐–ฐ๐–ฑ๐–ฒ๐–ณ๐–ด๐–ต๐–ถ๐–ท๐–ธ๐–น', @@ -122,7 +122,7 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response.status_code, 201) def test_returns_400_for_missing_name_param(self): - """Test does this return error message when name not provided.""" + """Return error message when name not provided.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.post(url) self.assertEqual(response.status_code, 400) @@ -131,7 +131,7 @@ class CreationTests(APISubdomainTestCase): }) def test_returns_400_for_bad_name_param(self): - """Test does this return error message when invalid characters provided.""" + """Return error message when invalid characters provided.""" url = reverse('bot:offtopicchannelname-list', host='api') invalid_names = ( 'space between words', @@ -154,21 +154,21 @@ class DeletionTests(APISubdomainTestCase): cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk') def test_deleting_unknown_name_returns_404(self): - """Test does this return 404 code when trying to delete unknown name.""" + """Return 404 reponse when trying to delete unknown name.""" url = reverse('bot:offtopicchannelname-detail', args=('unknown-name',), host='api') response = self.client.delete(url) self.assertEqual(response.status_code, 404) def test_deleting_known_name_returns_204(self): - """Test does this return 204 code when deleting was successful.""" + """Return 204 response when deleting was successful.""" url = reverse('bot:offtopicchannelname-detail', args=(self.test_name.name,), host='api') response = self.client.delete(url) self.assertEqual(response.status_code, 204) def test_name_gets_deleted(self): - """Test does name gets actually deleted.""" + """Name gets actually deleted.""" url = reverse('bot:offtopicchannelname-detail', args=(self.test_name_2.name,), host='api') response = self.client.delete(url) -- cgit v1.2.3 From 32342aca3309b4fd482841cb84e4f166152d5c33 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 29 Aug 2020 09:17:15 +0300 Subject: Fix mess with migrations --- .../api/migrations/0051_create_news_setting.py | 25 ---- .../api/migrations/0052_create_news_setting.py | 25 ++++ .../migrations/0052_offtopicchannelname_used.py | 18 --- .../api/migrations/0052_remove_user_avatar_hash.py | 17 --- .../migrations/0053_offtopicchannelname_used.py | 18 +++ .../api/migrations/0053_user_roles_to_array.py | 24 ---- .../api/migrations/0054_remove_user_avatar_hash.py | 17 +++ .../0054_user_invalidate_unknown_role.py | 21 --- .../api/migrations/0055_merge_20200714_2027.py | 14 -- .../apps/api/migrations/0055_reminder_mentions.py | 20 --- .../api/migrations/0055_user_roles_to_array.py | 24 ++++ .../api/migrations/0056_allow_blank_user_roles.py | 21 --- .../0056_user_invalidate_unknown_role.py | 21 +++ .../api/migrations/0057_merge_20200716_0751.py | 14 -- .../apps/api/migrations/0057_reminder_mentions.py | 20 +++ .../api/migrations/0058_allow_blank_user_roles.py | 21 +++ .../migrations/0058_create_new_filterlist_model.py | 33 ----- .../migrations/0059_create_new_filterlist_model.py | 33 +++++ .../api/migrations/0059_populate_filterlists.py | 153 --------------------- .../api/migrations/0060_populate_filterlists.py | 153 +++++++++++++++++++++ .../migrations/0060_populate_filterlists_fix.py | 85 ------------ .../migrations/0061_populate_filterlists_fix.py | 85 ++++++++++++ 22 files changed, 417 insertions(+), 445 deletions(-) delete mode 100644 pydis_site/apps/api/migrations/0051_create_news_setting.py create mode 100644 pydis_site/apps/api/migrations/0052_create_news_setting.py delete mode 100644 pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py delete mode 100644 pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py create mode 100644 pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py delete mode 100644 pydis_site/apps/api/migrations/0053_user_roles_to_array.py create mode 100644 pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py delete mode 100644 pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py delete mode 100644 pydis_site/apps/api/migrations/0055_merge_20200714_2027.py delete mode 100644 pydis_site/apps/api/migrations/0055_reminder_mentions.py create mode 100644 pydis_site/apps/api/migrations/0055_user_roles_to_array.py delete mode 100644 pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py create mode 100644 pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py delete mode 100644 pydis_site/apps/api/migrations/0057_merge_20200716_0751.py create mode 100644 pydis_site/apps/api/migrations/0057_reminder_mentions.py create mode 100644 pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py delete mode 100644 pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py create mode 100644 pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py delete mode 100644 pydis_site/apps/api/migrations/0059_populate_filterlists.py create mode 100644 pydis_site/apps/api/migrations/0060_populate_filterlists.py delete mode 100644 pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py create mode 100644 pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py diff --git a/pydis_site/apps/api/migrations/0051_create_news_setting.py b/pydis_site/apps/api/migrations/0051_create_news_setting.py deleted file mode 100644 index f18fdfb1..00000000 --- a/pydis_site/apps/api/migrations/0051_create_news_setting.py +++ /dev/null @@ -1,25 +0,0 @@ -from django.db import migrations - - -def up(apps, schema_editor): - BotSetting = apps.get_model('api', 'BotSetting') - setting = BotSetting( - name='news', - data={} - ).save() - - -def down(apps, schema_editor): - BotSetting = apps.get_model('api', 'BotSetting') - BotSetting.objects.get(name='news').delete() - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0050_remove_infractions_active_default_value'), - ] - - operations = [ - migrations.RunPython(up, down) - ] diff --git a/pydis_site/apps/api/migrations/0052_create_news_setting.py b/pydis_site/apps/api/migrations/0052_create_news_setting.py new file mode 100644 index 00000000..b101d19d --- /dev/null +++ b/pydis_site/apps/api/migrations/0052_create_news_setting.py @@ -0,0 +1,25 @@ +from django.db import migrations + + +def up(apps, schema_editor): + BotSetting = apps.get_model('api', 'BotSetting') + setting = BotSetting( + name='news', + data={} + ).save() + + +def down(apps, schema_editor): + BotSetting = apps.get_model('api', 'BotSetting') + BotSetting.objects.get(name='news').delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0051_allow_blank_message_embeds'), + ] + + operations = [ + migrations.RunPython(up, down) + ] diff --git a/pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py deleted file mode 100644 index dfdf3835..00000000 --- a/pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.11 on 2020-03-30 10:24 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0051_create_news_setting'), - ] - - operations = [ - migrations.AddField( - model_name='offtopicchannelname', - name='used', - field=models.BooleanField(default=False, help_text='Whether or not this name has already been used during this rotation'), - ), - ] diff --git a/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py deleted file mode 100644 index 26b3b954..00000000 --- a/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.2.11 on 2020-05-27 07:17 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0051_create_news_setting'), - ] - - operations = [ - migrations.RemoveField( - model_name='user', - name='avatar_hash', - ), - ] diff --git a/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py new file mode 100644 index 00000000..b51ce1d2 --- /dev/null +++ b/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.11 on 2020-03-30 10:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0052_create_news_setting'), + ] + + operations = [ + migrations.AddField( + model_name='offtopicchannelname', + name='used', + field=models.BooleanField(default=False, help_text='Whether or not this name has already been used during this rotation'), + ), + ] diff --git a/pydis_site/apps/api/migrations/0053_user_roles_to_array.py b/pydis_site/apps/api/migrations/0053_user_roles_to_array.py deleted file mode 100644 index 7ff3a548..00000000 --- a/pydis_site/apps/api/migrations/0053_user_roles_to_array.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.2.11 on 2020-06-02 13:42 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0052_remove_user_avatar_hash'), - ] - - operations = [ - migrations.RemoveField( - model_name='user', - name='roles', - ), - migrations.AddField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py new file mode 100644 index 00000000..be9fd948 --- /dev/null +++ b/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.11 on 2020-05-27 07:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0053_offtopicchannelname_used'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='avatar_hash', + ), + ] diff --git a/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py deleted file mode 100644 index 96230015..00000000 --- a/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 2.2.11 on 2020-06-02 20:08 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models -import pydis_site.apps.api.models.bot.user - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0053_user_roles_to_array'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py b/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py deleted file mode 100644 index f2a0e638..00000000 --- a/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-14 20:27 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0051_allow_blank_message_embeds'), - ('api', '0054_user_invalidate_unknown_role'), - ] - - operations = [ - ] diff --git a/pydis_site/apps/api/migrations/0055_reminder_mentions.py b/pydis_site/apps/api/migrations/0055_reminder_mentions.py deleted file mode 100644 index d73b450d..00000000 --- a/pydis_site/apps/api/migrations/0055_reminder_mentions.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.2.14 on 2020-07-15 07:37 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0054_user_invalidate_unknown_role'), - ] - - operations = [ - migrations.AddField( - model_name='reminder', - name='mentions', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0055_user_roles_to_array.py b/pydis_site/apps/api/migrations/0055_user_roles_to_array.py new file mode 100644 index 00000000..e7b4a983 --- /dev/null +++ b/pydis_site/apps/api/migrations/0055_user_roles_to_array.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.11 on 2020-06-02 13:42 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0054_remove_user_avatar_hash'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='roles', + ), + migrations.AddField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py deleted file mode 100644 index 489941c7..00000000 --- a/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-14 20:35 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models -import pydis_site.apps.api.models.bot.user - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0055_merge_20200714_2027'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py new file mode 100644 index 00000000..ab2696aa --- /dev/null +++ b/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.11 on 2020-06-02 20:08 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.user + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0055_user_roles_to_array'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py b/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py deleted file mode 100644 index 47a6d2d4..00000000 --- a/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 2.2.14 on 2020-07-16 07:51 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0055_reminder_mentions'), - ('api', '0056_allow_blank_user_roles'), - ] - - operations = [ - ] diff --git a/pydis_site/apps/api/migrations/0057_reminder_mentions.py b/pydis_site/apps/api/migrations/0057_reminder_mentions.py new file mode 100644 index 00000000..fb829a17 --- /dev/null +++ b/pydis_site/apps/api/migrations/0057_reminder_mentions.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.14 on 2020-07-15 07:37 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0056_user_invalidate_unknown_role'), + ] + + operations = [ + migrations.AddField( + model_name='reminder', + name='mentions', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py new file mode 100644 index 00000000..8f7fddfc --- /dev/null +++ b/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0.8 on 2020-07-14 20:35 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.user + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0057_reminder_mentions'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py deleted file mode 100644 index aecfdad7..00000000 --- a/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-15 11:23 - -from django.db import migrations, models -import pydis_site.apps.api.models.mixins - - -class Migration(migrations.Migration): - dependencies = [ - ('api', '0057_merge_20200716_0751'), - ] - - operations = [ - migrations.CreateModel( - name='FilterList', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('type', models.CharField( - choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), - ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], - help_text='The type of allowlist this is on.', max_length=50)), - ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), - ('content', models.TextField(help_text='The data to add to the allow or denylist.')), - ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), - ], - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - migrations.AddConstraint( - model_name='filterlist', - constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list') - ) - ] diff --git a/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py new file mode 100644 index 00000000..eac5542d --- /dev/null +++ b/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py @@ -0,0 +1,33 @@ +# Generated by Django 3.0.8 on 2020-07-15 11:23 + +from django.db import migrations, models +import pydis_site.apps.api.models.mixins + + +class Migration(migrations.Migration): + dependencies = [ + ('api', '0058_allow_blank_user_roles'), + ] + + operations = [ + migrations.CreateModel( + name='FilterList', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('type', models.CharField( + choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), + ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], + help_text='The type of allowlist this is on.', max_length=50)), + ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), + ('content', models.TextField(help_text='The data to add to the allow or denylist.')), + ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), + ], + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), + ), + migrations.AddConstraint( + model_name='filterlist', + constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list') + ) + ] diff --git a/pydis_site/apps/api/migrations/0059_populate_filterlists.py b/pydis_site/apps/api/migrations/0059_populate_filterlists.py deleted file mode 100644 index 8c550191..00000000 --- a/pydis_site/apps/api/migrations/0059_populate_filterlists.py +++ /dev/null @@ -1,153 +0,0 @@ -from django.db import migrations - -guild_invite_whitelist = [ - ("discord.gg/python", "Python Discord", True), - ("discord.gg/4JJdJKb", "RLBot", True), - ("discord.gg/djPtTRJ", "Kivy", True), - ("discord.gg/QXyegWe", "Pyglet", True), - ("discord.gg/9XsucTT", "Panda3D", True), - ("discord.gg/AP3rq2k", "PyWeek", True), - ("discord.gg/vSPsP9t", "Microsoft Python", True), - ("discord.gg/bRCvFy9", "Discord.js Official", True), - ("discord.gg/9zT7NHP", "Programming Discussions", True), - ("discord.gg/ysd6M4r", "JetBrains Community", True), - ("discord.gg/4xJeCgy", "Raspberry Pie", True), - ("discord.gg/AStb3kZ", "Ren'Py", True), - ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), - ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), - ("discord.gg/jTtgWuy", "Django", True), - ("discord.gg/W9BypZF", "STEM", True), - ("discord.gg/dpy", "discord.py", True), - ("discord.gg/programming", "Programmers Hangout", True), - ("discord.gg/qhGUjGD", "SpeakJS", True), - ("discord.gg/eTbWSZj", "Functional Programming", True), - ("discord.gg/r8yreB6", "PyGame", True), - ("discord.gg/5UBnR3P", "Python Atlanta", True), - ("discord.gg/ccyrDKv", "C#", True), -] - -domain_name_blacklist = [ - ("pornhub.com", None, False), - ("liveleak.com", None, False), - ("grabify.link", None, False), - ("bmwforum.co", None, False), - ("leancoding.co", None, False), - ("spottyfly.com", None, False), - ("stopify.co", None, False), - ("yoรผtu.be", None, False), - ("discรถrd.com", None, False), - ("minecrรคft.com", None, False), - ("freegiftcards.co", None, False), - ("disรงordapp.com", None, False), - ("fortnight.space", None, False), - ("fortnitechat.site", None, False), - ("joinmy.site", None, False), - ("curiouscat.club", None, False), - ("catsnthings.fun", None, False), - ("yourtube.site", None, False), - ("youtubeshort.watch", None, False), - ("catsnthing.com", None, False), - ("youtubeshort.pro", None, False), - ("canadianlumberjacks.online", None, False), - ("poweredbydialup.club", None, False), - ("poweredbydialup.online", None, False), - ("poweredbysecurity.org", None, False), - ("poweredbysecurity.online", None, False), - ("ssteam.site", None, False), - ("steamwalletgift.com", None, False), - ("discord.gift", None, False), - ("lmgtfy.com", None, False), -] - -filter_token_blacklist = [ - ("\bgoo+ks*\b", None, False), - ("\bky+s+\b", None, False), - ("\bki+ke+s*\b", None, False), - ("\bbeaner+s?\b", None, False), - ("\bcoo+ns*\b", None, False), - ("\bnig+lets*\b", None, False), - ("\bslant-eyes*\b", None, False), - ("\btowe?l-?head+s*\b", None, False), - ("\bchi*n+k+s*\b", None, False), - ("\bspick*s*\b", None, False), - ("\bkill* +(?:yo)?urself+\b", None, False), - ("\bjew+s*\b", None, False), - ("\bsuicide\b", None, False), - ("\brape\b", None, False), - ("\b(re+)tar+(d+|t+)(ed)?\b", None, False), - ("\bta+r+d+\b", None, False), - ("\bcunts*\b", None, False), - ("\btrann*y\b", None, False), - ("\bshemale\b", None, False), - ("fa+g+s*", None, False), - ("ๅ", None, False), - ("ๅ", None, False), - ("เฟ–", None, False), - ("เฟ•", None, False), - ("เฟ˜", None, False), - ("เฟ—", None, False), - ("cuck(?!oo+)", None, False), - ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), - ("fag+o+t+s*", None, False), -] - -file_format_whitelist = [ - (".3gp", None, True), - (".3g2", None, True), - (".avi", None, True), - (".bmp", None, True), - (".gif", None, True), - (".h264", None, True), - (".jpg", None, True), - (".jpeg", None, True), - (".m4v", None, True), - (".mkv", None, True), - (".mov", None, True), - (".mp4", None, True), - (".mpeg", None, True), - (".mpg", None, True), - (".png", None, True), - (".tiff", None, True), - (".wmv", None, True), - (".svg", None, True), - (".psd", "Photoshop", True), - (".ai", "Illustrator", True), - (".aep", "After Effects", True), - (".xcf", "GIMP", True), - (".mp3", None, True), - (".wav", None, True), - (".ogg", None, True), - (".webm", None, True), - (".webp", None, True), -] - -populate_data = { - "FILTER_TOKEN": filter_token_blacklist, - "DOMAIN_NAME": domain_name_blacklist, - "FILE_FORMAT": file_format_whitelist, - "GUILD_INVITE": guild_invite_whitelist, -} - - -class Migration(migrations.Migration): - dependencies = [("api", "0058_create_new_filterlist_model")] - - def populate_filterlists(app, _): - FilterList = app.get_model("api", "FilterList") - - for filterlist_type, metadata in populate_data.items(): - for content, comment, allowed in metadata: - FilterList.objects.create( - type=filterlist_type, - allowed=allowed, - content=content, - comment=comment, - ) - - def clear_filterlists(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.all().delete() - - operations = [ - migrations.RunPython(populate_filterlists, clear_filterlists) - ] diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists.py b/pydis_site/apps/api/migrations/0060_populate_filterlists.py new file mode 100644 index 00000000..35fde95a --- /dev/null +++ b/pydis_site/apps/api/migrations/0060_populate_filterlists.py @@ -0,0 +1,153 @@ +from django.db import migrations + +guild_invite_whitelist = [ + ("discord.gg/python", "Python Discord", True), + ("discord.gg/4JJdJKb", "RLBot", True), + ("discord.gg/djPtTRJ", "Kivy", True), + ("discord.gg/QXyegWe", "Pyglet", True), + ("discord.gg/9XsucTT", "Panda3D", True), + ("discord.gg/AP3rq2k", "PyWeek", True), + ("discord.gg/vSPsP9t", "Microsoft Python", True), + ("discord.gg/bRCvFy9", "Discord.js Official", True), + ("discord.gg/9zT7NHP", "Programming Discussions", True), + ("discord.gg/ysd6M4r", "JetBrains Community", True), + ("discord.gg/4xJeCgy", "Raspberry Pie", True), + ("discord.gg/AStb3kZ", "Ren'Py", True), + ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), + ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), + ("discord.gg/jTtgWuy", "Django", True), + ("discord.gg/W9BypZF", "STEM", True), + ("discord.gg/dpy", "discord.py", True), + ("discord.gg/programming", "Programmers Hangout", True), + ("discord.gg/qhGUjGD", "SpeakJS", True), + ("discord.gg/eTbWSZj", "Functional Programming", True), + ("discord.gg/r8yreB6", "PyGame", True), + ("discord.gg/5UBnR3P", "Python Atlanta", True), + ("discord.gg/ccyrDKv", "C#", True), +] + +domain_name_blacklist = [ + ("pornhub.com", None, False), + ("liveleak.com", None, False), + ("grabify.link", None, False), + ("bmwforum.co", None, False), + ("leancoding.co", None, False), + ("spottyfly.com", None, False), + ("stopify.co", None, False), + ("yoรผtu.be", None, False), + ("discรถrd.com", None, False), + ("minecrรคft.com", None, False), + ("freegiftcards.co", None, False), + ("disรงordapp.com", None, False), + ("fortnight.space", None, False), + ("fortnitechat.site", None, False), + ("joinmy.site", None, False), + ("curiouscat.club", None, False), + ("catsnthings.fun", None, False), + ("yourtube.site", None, False), + ("youtubeshort.watch", None, False), + ("catsnthing.com", None, False), + ("youtubeshort.pro", None, False), + ("canadianlumberjacks.online", None, False), + ("poweredbydialup.club", None, False), + ("poweredbydialup.online", None, False), + ("poweredbysecurity.org", None, False), + ("poweredbysecurity.online", None, False), + ("ssteam.site", None, False), + ("steamwalletgift.com", None, False), + ("discord.gift", None, False), + ("lmgtfy.com", None, False), +] + +filter_token_blacklist = [ + ("\bgoo+ks*\b", None, False), + ("\bky+s+\b", None, False), + ("\bki+ke+s*\b", None, False), + ("\bbeaner+s?\b", None, False), + ("\bcoo+ns*\b", None, False), + ("\bnig+lets*\b", None, False), + ("\bslant-eyes*\b", None, False), + ("\btowe?l-?head+s*\b", None, False), + ("\bchi*n+k+s*\b", None, False), + ("\bspick*s*\b", None, False), + ("\bkill* +(?:yo)?urself+\b", None, False), + ("\bjew+s*\b", None, False), + ("\bsuicide\b", None, False), + ("\brape\b", None, False), + ("\b(re+)tar+(d+|t+)(ed)?\b", None, False), + ("\bta+r+d+\b", None, False), + ("\bcunts*\b", None, False), + ("\btrann*y\b", None, False), + ("\bshemale\b", None, False), + ("fa+g+s*", None, False), + ("ๅ", None, False), + ("ๅ", None, False), + ("เฟ–", None, False), + ("เฟ•", None, False), + ("เฟ˜", None, False), + ("เฟ—", None, False), + ("cuck(?!oo+)", None, False), + ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), + ("fag+o+t+s*", None, False), +] + +file_format_whitelist = [ + (".3gp", None, True), + (".3g2", None, True), + (".avi", None, True), + (".bmp", None, True), + (".gif", None, True), + (".h264", None, True), + (".jpg", None, True), + (".jpeg", None, True), + (".m4v", None, True), + (".mkv", None, True), + (".mov", None, True), + (".mp4", None, True), + (".mpeg", None, True), + (".mpg", None, True), + (".png", None, True), + (".tiff", None, True), + (".wmv", None, True), + (".svg", None, True), + (".psd", "Photoshop", True), + (".ai", "Illustrator", True), + (".aep", "After Effects", True), + (".xcf", "GIMP", True), + (".mp3", None, True), + (".wav", None, True), + (".ogg", None, True), + (".webm", None, True), + (".webp", None, True), +] + +populate_data = { + "FILTER_TOKEN": filter_token_blacklist, + "DOMAIN_NAME": domain_name_blacklist, + "FILE_FORMAT": file_format_whitelist, + "GUILD_INVITE": guild_invite_whitelist, +} + + +class Migration(migrations.Migration): + dependencies = [("api", "0059_create_new_filterlist_model")] + + def populate_filterlists(app, _): + FilterList = app.get_model("api", "FilterList") + + for filterlist_type, metadata in populate_data.items(): + for content, comment, allowed in metadata: + FilterList.objects.create( + type=filterlist_type, + allowed=allowed, + content=content, + comment=comment, + ) + + def clear_filterlists(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.all().delete() + + operations = [ + migrations.RunPython(populate_filterlists, clear_filterlists) + ] diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py deleted file mode 100644 index 53846f02..00000000 --- a/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py +++ /dev/null @@ -1,85 +0,0 @@ -from django.db import migrations - -bad_guild_invite_whitelist = [ - ("discord.gg/python", "Python Discord", True), - ("discord.gg/4JJdJKb", "RLBot", True), - ("discord.gg/djPtTRJ", "Kivy", True), - ("discord.gg/QXyegWe", "Pyglet", True), - ("discord.gg/9XsucTT", "Panda3D", True), - ("discord.gg/AP3rq2k", "PyWeek", True), - ("discord.gg/vSPsP9t", "Microsoft Python", True), - ("discord.gg/bRCvFy9", "Discord.js Official", True), - ("discord.gg/9zT7NHP", "Programming Discussions", True), - ("discord.gg/ysd6M4r", "JetBrains Community", True), - ("discord.gg/4xJeCgy", "Raspberry Pie", True), - ("discord.gg/AStb3kZ", "Ren'Py", True), - ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), - ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), - ("discord.gg/jTtgWuy", "Django", True), - ("discord.gg/W9BypZF", "STEM", True), - ("discord.gg/dpy", "discord.py", True), - ("discord.gg/programming", "Programmers Hangout", True), - ("discord.gg/qhGUjGD", "SpeakJS", True), - ("discord.gg/eTbWSZj", "Functional Programming", True), - ("discord.gg/r8yreB6", "PyGame", True), - ("discord.gg/5UBnR3P", "Python Atlanta", True), - ("discord.gg/ccyrDKv", "C#", True), -] - -guild_invite_whitelist = [ - ("267624335836053506", "Python Discord", True), - ("348658686962696195", "RLBot", True), - ("423249981340778496", "Kivy", True), - ("438622377094414346", "Pyglet", True), - ("524691714909274162", "Panda3D", True), - ("666560367173828639", "PyWeek", True), - ("702724176489873509", "Microsoft Python", True), - ("222078108977594368", "Discord.js Official", True), - ("238666723824238602", "Programming Discussions", True), - ("433980600391696384", "JetBrains Community", True), - ("204621105720328193", "Raspberry Pie", True), - ("286633898581164032", "Ren'Py", True), - ("440186186024222721", "Python Discord: Emojis 1", True), - ("578587418123304970", "Python Discord: Emojis 2", True), - ("159039020565790721", "Django", True), - ("273944235143593984", "STEM", True), - ("336642139381301249", "discord.py", True), - ("244230771232079873", "Programmers Hangout", True), - ("239433591950540801", "SpeakJS", True), - ("280033776820813825", "Functional Programming", True), - ("349505959032389632", "PyGame", True), - ("488751051629920277", "Python Atlanta", True), - ("143867839282020352", "C#", True), -] - - -class Migration(migrations.Migration): - dependencies = [("api", "0059_populate_filterlists")] - - def fix_filterlist(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.filter(type="GUILD_INVITE").delete() # Clear out the stuff added in 0059. - - for content, comment, allowed in guild_invite_whitelist: - FilterList.objects.create( - type="GUILD_INVITE", - allowed=allowed, - content=content, - comment=comment, - ) - - def restore_bad_filterlist(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.filter(type="GUILD_INVITE").delete() - - for content, comment, allowed in bad_guild_invite_whitelist: - FilterList.objects.create( - type="GUILD_INVITE", - allowed=allowed, - content=content, - comment=comment, - ) - - operations = [ - migrations.RunPython(fix_filterlist, restore_bad_filterlist) - ] diff --git a/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py new file mode 100644 index 00000000..eaaafb38 --- /dev/null +++ b/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py @@ -0,0 +1,85 @@ +from django.db import migrations + +bad_guild_invite_whitelist = [ + ("discord.gg/python", "Python Discord", True), + ("discord.gg/4JJdJKb", "RLBot", True), + ("discord.gg/djPtTRJ", "Kivy", True), + ("discord.gg/QXyegWe", "Pyglet", True), + ("discord.gg/9XsucTT", "Panda3D", True), + ("discord.gg/AP3rq2k", "PyWeek", True), + ("discord.gg/vSPsP9t", "Microsoft Python", True), + ("discord.gg/bRCvFy9", "Discord.js Official", True), + ("discord.gg/9zT7NHP", "Programming Discussions", True), + ("discord.gg/ysd6M4r", "JetBrains Community", True), + ("discord.gg/4xJeCgy", "Raspberry Pie", True), + ("discord.gg/AStb3kZ", "Ren'Py", True), + ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), + ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), + ("discord.gg/jTtgWuy", "Django", True), + ("discord.gg/W9BypZF", "STEM", True), + ("discord.gg/dpy", "discord.py", True), + ("discord.gg/programming", "Programmers Hangout", True), + ("discord.gg/qhGUjGD", "SpeakJS", True), + ("discord.gg/eTbWSZj", "Functional Programming", True), + ("discord.gg/r8yreB6", "PyGame", True), + ("discord.gg/5UBnR3P", "Python Atlanta", True), + ("discord.gg/ccyrDKv", "C#", True), +] + +guild_invite_whitelist = [ + ("267624335836053506", "Python Discord", True), + ("348658686962696195", "RLBot", True), + ("423249981340778496", "Kivy", True), + ("438622377094414346", "Pyglet", True), + ("524691714909274162", "Panda3D", True), + ("666560367173828639", "PyWeek", True), + ("702724176489873509", "Microsoft Python", True), + ("222078108977594368", "Discord.js Official", True), + ("238666723824238602", "Programming Discussions", True), + ("433980600391696384", "JetBrains Community", True), + ("204621105720328193", "Raspberry Pie", True), + ("286633898581164032", "Ren'Py", True), + ("440186186024222721", "Python Discord: Emojis 1", True), + ("578587418123304970", "Python Discord: Emojis 2", True), + ("159039020565790721", "Django", True), + ("273944235143593984", "STEM", True), + ("336642139381301249", "discord.py", True), + ("244230771232079873", "Programmers Hangout", True), + ("239433591950540801", "SpeakJS", True), + ("280033776820813825", "Functional Programming", True), + ("349505959032389632", "PyGame", True), + ("488751051629920277", "Python Atlanta", True), + ("143867839282020352", "C#", True), +] + + +class Migration(migrations.Migration): + dependencies = [("api", "0060_populate_filterlists")] + + def fix_filterlist(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.filter(type="GUILD_INVITE").delete() # Clear out the stuff added in 0059. + + for content, comment, allowed in guild_invite_whitelist: + FilterList.objects.create( + type="GUILD_INVITE", + allowed=allowed, + content=content, + comment=comment, + ) + + def restore_bad_filterlist(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.filter(type="GUILD_INVITE").delete() + + for content, comment, allowed in bad_guild_invite_whitelist: + FilterList.objects.create( + type="GUILD_INVITE", + allowed=allowed, + content=content, + comment=comment, + ) + + operations = [ + migrations.RunPython(fix_filterlist, restore_bad_filterlist) + ] -- cgit v1.2.3 From ac7745e4ea79fc129447e9b472d3dbd49bf00fd6 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 29 Aug 2020 09:21:15 +0300 Subject: Fix linting issues on off-topic viewset --- pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py index 29978015..826ad25e 100644 --- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py @@ -1,4 +1,4 @@ -from django.db.models import Case, When, Value +from django.db.models import Case, Value, When from django.db.models.query import QuerySet from django.http.request import HttpRequest from django.shortcuts import get_object_or_404 @@ -114,9 +114,13 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): # When any name is used in our listing then this means we reached end of round # and we need to reset all other names `used` to False if any(offtopic_name.used for offtopic_name in queryset): + # These names that we just got have to be excluded from updating used to False self.get_queryset().update( - used=Case( # These names that we just got have to be excluded from updating to False - When(name__in=(offtopic_name.name for offtopic_name in queryset), then=Value(True)), + used=Case( + When( + name__in=(offtopic_name.name for offtopic_name in queryset), + then=Value(True) + ), default=Value(False) ) ) -- cgit v1.2.3 From dae68c46831ef14e6a0715add1025f9af1b5a73d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 29 Aug 2020 10:00:45 -0700 Subject: Revert pull request #348 Revert commit 330a27926a7f2a9bdae56a5928cd81e55ccb38ed to reverse changes made to 6c08da2f3e48db9c9dd452f2e0505c4a61e46592. Migrations were failing when those changes were deployed. --- .../api/migrations/0051_create_news_setting.py | 25 ++++ .../api/migrations/0052_create_news_setting.py | 25 ---- .../api/migrations/0052_remove_user_avatar_hash.py | 17 +++ .../migrations/0053_offtopicchannelname_used.py | 18 --- .../api/migrations/0053_user_roles_to_array.py | 24 ++++ .../api/migrations/0054_remove_user_avatar_hash.py | 17 --- .../0054_user_invalidate_unknown_role.py | 21 +++ .../api/migrations/0055_merge_20200714_2027.py | 14 ++ .../apps/api/migrations/0055_reminder_mentions.py | 20 +++ .../api/migrations/0055_user_roles_to_array.py | 24 ---- .../api/migrations/0056_allow_blank_user_roles.py | 21 +++ .../0056_user_invalidate_unknown_role.py | 21 --- .../api/migrations/0057_merge_20200716_0751.py | 14 ++ .../apps/api/migrations/0057_reminder_mentions.py | 20 --- .../api/migrations/0058_allow_blank_user_roles.py | 21 --- .../migrations/0058_create_new_filterlist_model.py | 33 +++++ .../migrations/0059_create_new_filterlist_model.py | 33 ----- .../api/migrations/0059_populate_filterlists.py | 153 +++++++++++++++++++++ .../api/migrations/0060_populate_filterlists.py | 153 --------------------- .../migrations/0060_populate_filterlists_fix.py | 85 ++++++++++++ .../migrations/0061_populate_filterlists_fix.py | 85 ------------ .../apps/api/models/bot/off_topic_channel_name.py | 5 - .../apps/api/tests/test_off_topic_channel_names.py | 27 +--- .../api/viewsets/bot/off_topic_channel_name.py | 27 +--- 24 files changed, 431 insertions(+), 472 deletions(-) create mode 100644 pydis_site/apps/api/migrations/0051_create_news_setting.py delete mode 100644 pydis_site/apps/api/migrations/0052_create_news_setting.py create mode 100644 pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py delete mode 100644 pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py create mode 100644 pydis_site/apps/api/migrations/0053_user_roles_to_array.py delete mode 100644 pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py create mode 100644 pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py create mode 100644 pydis_site/apps/api/migrations/0055_merge_20200714_2027.py create mode 100644 pydis_site/apps/api/migrations/0055_reminder_mentions.py delete mode 100644 pydis_site/apps/api/migrations/0055_user_roles_to_array.py create mode 100644 pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py delete mode 100644 pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py create mode 100644 pydis_site/apps/api/migrations/0057_merge_20200716_0751.py delete mode 100644 pydis_site/apps/api/migrations/0057_reminder_mentions.py delete mode 100644 pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py create mode 100644 pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py delete mode 100644 pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py create mode 100644 pydis_site/apps/api/migrations/0059_populate_filterlists.py delete mode 100644 pydis_site/apps/api/migrations/0060_populate_filterlists.py create mode 100644 pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py delete mode 100644 pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py diff --git a/pydis_site/apps/api/migrations/0051_create_news_setting.py b/pydis_site/apps/api/migrations/0051_create_news_setting.py new file mode 100644 index 00000000..f18fdfb1 --- /dev/null +++ b/pydis_site/apps/api/migrations/0051_create_news_setting.py @@ -0,0 +1,25 @@ +from django.db import migrations + + +def up(apps, schema_editor): + BotSetting = apps.get_model('api', 'BotSetting') + setting = BotSetting( + name='news', + data={} + ).save() + + +def down(apps, schema_editor): + BotSetting = apps.get_model('api', 'BotSetting') + BotSetting.objects.get(name='news').delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0050_remove_infractions_active_default_value'), + ] + + operations = [ + migrations.RunPython(up, down) + ] diff --git a/pydis_site/apps/api/migrations/0052_create_news_setting.py b/pydis_site/apps/api/migrations/0052_create_news_setting.py deleted file mode 100644 index b101d19d..00000000 --- a/pydis_site/apps/api/migrations/0052_create_news_setting.py +++ /dev/null @@ -1,25 +0,0 @@ -from django.db import migrations - - -def up(apps, schema_editor): - BotSetting = apps.get_model('api', 'BotSetting') - setting = BotSetting( - name='news', - data={} - ).save() - - -def down(apps, schema_editor): - BotSetting = apps.get_model('api', 'BotSetting') - BotSetting.objects.get(name='news').delete() - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0051_allow_blank_message_embeds'), - ] - - operations = [ - migrations.RunPython(up, down) - ] diff --git a/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py new file mode 100644 index 00000000..26b3b954 --- /dev/null +++ b/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.11 on 2020-05-27 07:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0051_create_news_setting'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='avatar_hash', + ), + ] diff --git a/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py deleted file mode 100644 index b51ce1d2..00000000 --- a/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.11 on 2020-03-30 10:24 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0052_create_news_setting'), - ] - - operations = [ - migrations.AddField( - model_name='offtopicchannelname', - name='used', - field=models.BooleanField(default=False, help_text='Whether or not this name has already been used during this rotation'), - ), - ] diff --git a/pydis_site/apps/api/migrations/0053_user_roles_to_array.py b/pydis_site/apps/api/migrations/0053_user_roles_to_array.py new file mode 100644 index 00000000..7ff3a548 --- /dev/null +++ b/pydis_site/apps/api/migrations/0053_user_roles_to_array.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.11 on 2020-06-02 13:42 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0052_remove_user_avatar_hash'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='roles', + ), + migrations.AddField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py deleted file mode 100644 index be9fd948..00000000 --- a/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.2.11 on 2020-05-27 07:17 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0053_offtopicchannelname_used'), - ] - - operations = [ - migrations.RemoveField( - model_name='user', - name='avatar_hash', - ), - ] diff --git a/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py new file mode 100644 index 00000000..96230015 --- /dev/null +++ b/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.11 on 2020-06-02 20:08 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.user + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0053_user_roles_to_array'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py b/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py new file mode 100644 index 00000000..f2a0e638 --- /dev/null +++ b/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.8 on 2020-07-14 20:27 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0051_allow_blank_message_embeds'), + ('api', '0054_user_invalidate_unknown_role'), + ] + + operations = [ + ] diff --git a/pydis_site/apps/api/migrations/0055_reminder_mentions.py b/pydis_site/apps/api/migrations/0055_reminder_mentions.py new file mode 100644 index 00000000..d73b450d --- /dev/null +++ b/pydis_site/apps/api/migrations/0055_reminder_mentions.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.14 on 2020-07-15 07:37 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0054_user_invalidate_unknown_role'), + ] + + operations = [ + migrations.AddField( + model_name='reminder', + name='mentions', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0055_user_roles_to_array.py b/pydis_site/apps/api/migrations/0055_user_roles_to_array.py deleted file mode 100644 index e7b4a983..00000000 --- a/pydis_site/apps/api/migrations/0055_user_roles_to_array.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.2.11 on 2020-06-02 13:42 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0054_remove_user_avatar_hash'), - ] - - operations = [ - migrations.RemoveField( - model_name='user', - name='roles', - ), - migrations.AddField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py new file mode 100644 index 00000000..489941c7 --- /dev/null +++ b/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0.8 on 2020-07-14 20:35 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.user + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0055_merge_20200714_2027'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py deleted file mode 100644 index ab2696aa..00000000 --- a/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 2.2.11 on 2020-06-02 20:08 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models -import pydis_site.apps.api.models.bot.user - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0055_user_roles_to_array'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py b/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py new file mode 100644 index 00000000..47a6d2d4 --- /dev/null +++ b/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.14 on 2020-07-16 07:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0055_reminder_mentions'), + ('api', '0056_allow_blank_user_roles'), + ] + + operations = [ + ] diff --git a/pydis_site/apps/api/migrations/0057_reminder_mentions.py b/pydis_site/apps/api/migrations/0057_reminder_mentions.py deleted file mode 100644 index fb829a17..00000000 --- a/pydis_site/apps/api/migrations/0057_reminder_mentions.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.2.14 on 2020-07-15 07:37 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0056_user_invalidate_unknown_role'), - ] - - operations = [ - migrations.AddField( - model_name='reminder', - name='mentions', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py deleted file mode 100644 index 8f7fddfc..00000000 --- a/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-14 20:35 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models -import pydis_site.apps.api.models.bot.user - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0057_reminder_mentions'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py new file mode 100644 index 00000000..aecfdad7 --- /dev/null +++ b/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py @@ -0,0 +1,33 @@ +# Generated by Django 3.0.8 on 2020-07-15 11:23 + +from django.db import migrations, models +import pydis_site.apps.api.models.mixins + + +class Migration(migrations.Migration): + dependencies = [ + ('api', '0057_merge_20200716_0751'), + ] + + operations = [ + migrations.CreateModel( + name='FilterList', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('type', models.CharField( + choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), + ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], + help_text='The type of allowlist this is on.', max_length=50)), + ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), + ('content', models.TextField(help_text='The data to add to the allow or denylist.')), + ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), + ], + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), + ), + migrations.AddConstraint( + model_name='filterlist', + constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list') + ) + ] diff --git a/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py deleted file mode 100644 index eac5542d..00000000 --- a/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-15 11:23 - -from django.db import migrations, models -import pydis_site.apps.api.models.mixins - - -class Migration(migrations.Migration): - dependencies = [ - ('api', '0058_allow_blank_user_roles'), - ] - - operations = [ - migrations.CreateModel( - name='FilterList', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('type', models.CharField( - choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), - ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], - help_text='The type of allowlist this is on.', max_length=50)), - ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), - ('content', models.TextField(help_text='The data to add to the allow or denylist.')), - ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), - ], - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - migrations.AddConstraint( - model_name='filterlist', - constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list') - ) - ] diff --git a/pydis_site/apps/api/migrations/0059_populate_filterlists.py b/pydis_site/apps/api/migrations/0059_populate_filterlists.py new file mode 100644 index 00000000..8c550191 --- /dev/null +++ b/pydis_site/apps/api/migrations/0059_populate_filterlists.py @@ -0,0 +1,153 @@ +from django.db import migrations + +guild_invite_whitelist = [ + ("discord.gg/python", "Python Discord", True), + ("discord.gg/4JJdJKb", "RLBot", True), + ("discord.gg/djPtTRJ", "Kivy", True), + ("discord.gg/QXyegWe", "Pyglet", True), + ("discord.gg/9XsucTT", "Panda3D", True), + ("discord.gg/AP3rq2k", "PyWeek", True), + ("discord.gg/vSPsP9t", "Microsoft Python", True), + ("discord.gg/bRCvFy9", "Discord.js Official", True), + ("discord.gg/9zT7NHP", "Programming Discussions", True), + ("discord.gg/ysd6M4r", "JetBrains Community", True), + ("discord.gg/4xJeCgy", "Raspberry Pie", True), + ("discord.gg/AStb3kZ", "Ren'Py", True), + ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), + ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), + ("discord.gg/jTtgWuy", "Django", True), + ("discord.gg/W9BypZF", "STEM", True), + ("discord.gg/dpy", "discord.py", True), + ("discord.gg/programming", "Programmers Hangout", True), + ("discord.gg/qhGUjGD", "SpeakJS", True), + ("discord.gg/eTbWSZj", "Functional Programming", True), + ("discord.gg/r8yreB6", "PyGame", True), + ("discord.gg/5UBnR3P", "Python Atlanta", True), + ("discord.gg/ccyrDKv", "C#", True), +] + +domain_name_blacklist = [ + ("pornhub.com", None, False), + ("liveleak.com", None, False), + ("grabify.link", None, False), + ("bmwforum.co", None, False), + ("leancoding.co", None, False), + ("spottyfly.com", None, False), + ("stopify.co", None, False), + ("yoรผtu.be", None, False), + ("discรถrd.com", None, False), + ("minecrรคft.com", None, False), + ("freegiftcards.co", None, False), + ("disรงordapp.com", None, False), + ("fortnight.space", None, False), + ("fortnitechat.site", None, False), + ("joinmy.site", None, False), + ("curiouscat.club", None, False), + ("catsnthings.fun", None, False), + ("yourtube.site", None, False), + ("youtubeshort.watch", None, False), + ("catsnthing.com", None, False), + ("youtubeshort.pro", None, False), + ("canadianlumberjacks.online", None, False), + ("poweredbydialup.club", None, False), + ("poweredbydialup.online", None, False), + ("poweredbysecurity.org", None, False), + ("poweredbysecurity.online", None, False), + ("ssteam.site", None, False), + ("steamwalletgift.com", None, False), + ("discord.gift", None, False), + ("lmgtfy.com", None, False), +] + +filter_token_blacklist = [ + ("\bgoo+ks*\b", None, False), + ("\bky+s+\b", None, False), + ("\bki+ke+s*\b", None, False), + ("\bbeaner+s?\b", None, False), + ("\bcoo+ns*\b", None, False), + ("\bnig+lets*\b", None, False), + ("\bslant-eyes*\b", None, False), + ("\btowe?l-?head+s*\b", None, False), + ("\bchi*n+k+s*\b", None, False), + ("\bspick*s*\b", None, False), + ("\bkill* +(?:yo)?urself+\b", None, False), + ("\bjew+s*\b", None, False), + ("\bsuicide\b", None, False), + ("\brape\b", None, False), + ("\b(re+)tar+(d+|t+)(ed)?\b", None, False), + ("\bta+r+d+\b", None, False), + ("\bcunts*\b", None, False), + ("\btrann*y\b", None, False), + ("\bshemale\b", None, False), + ("fa+g+s*", None, False), + ("ๅ", None, False), + ("ๅ", None, False), + ("เฟ–", None, False), + ("เฟ•", None, False), + ("เฟ˜", None, False), + ("เฟ—", None, False), + ("cuck(?!oo+)", None, False), + ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), + ("fag+o+t+s*", None, False), +] + +file_format_whitelist = [ + (".3gp", None, True), + (".3g2", None, True), + (".avi", None, True), + (".bmp", None, True), + (".gif", None, True), + (".h264", None, True), + (".jpg", None, True), + (".jpeg", None, True), + (".m4v", None, True), + (".mkv", None, True), + (".mov", None, True), + (".mp4", None, True), + (".mpeg", None, True), + (".mpg", None, True), + (".png", None, True), + (".tiff", None, True), + (".wmv", None, True), + (".svg", None, True), + (".psd", "Photoshop", True), + (".ai", "Illustrator", True), + (".aep", "After Effects", True), + (".xcf", "GIMP", True), + (".mp3", None, True), + (".wav", None, True), + (".ogg", None, True), + (".webm", None, True), + (".webp", None, True), +] + +populate_data = { + "FILTER_TOKEN": filter_token_blacklist, + "DOMAIN_NAME": domain_name_blacklist, + "FILE_FORMAT": file_format_whitelist, + "GUILD_INVITE": guild_invite_whitelist, +} + + +class Migration(migrations.Migration): + dependencies = [("api", "0058_create_new_filterlist_model")] + + def populate_filterlists(app, _): + FilterList = app.get_model("api", "FilterList") + + for filterlist_type, metadata in populate_data.items(): + for content, comment, allowed in metadata: + FilterList.objects.create( + type=filterlist_type, + allowed=allowed, + content=content, + comment=comment, + ) + + def clear_filterlists(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.all().delete() + + operations = [ + migrations.RunPython(populate_filterlists, clear_filterlists) + ] diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists.py b/pydis_site/apps/api/migrations/0060_populate_filterlists.py deleted file mode 100644 index 35fde95a..00000000 --- a/pydis_site/apps/api/migrations/0060_populate_filterlists.py +++ /dev/null @@ -1,153 +0,0 @@ -from django.db import migrations - -guild_invite_whitelist = [ - ("discord.gg/python", "Python Discord", True), - ("discord.gg/4JJdJKb", "RLBot", True), - ("discord.gg/djPtTRJ", "Kivy", True), - ("discord.gg/QXyegWe", "Pyglet", True), - ("discord.gg/9XsucTT", "Panda3D", True), - ("discord.gg/AP3rq2k", "PyWeek", True), - ("discord.gg/vSPsP9t", "Microsoft Python", True), - ("discord.gg/bRCvFy9", "Discord.js Official", True), - ("discord.gg/9zT7NHP", "Programming Discussions", True), - ("discord.gg/ysd6M4r", "JetBrains Community", True), - ("discord.gg/4xJeCgy", "Raspberry Pie", True), - ("discord.gg/AStb3kZ", "Ren'Py", True), - ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), - ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), - ("discord.gg/jTtgWuy", "Django", True), - ("discord.gg/W9BypZF", "STEM", True), - ("discord.gg/dpy", "discord.py", True), - ("discord.gg/programming", "Programmers Hangout", True), - ("discord.gg/qhGUjGD", "SpeakJS", True), - ("discord.gg/eTbWSZj", "Functional Programming", True), - ("discord.gg/r8yreB6", "PyGame", True), - ("discord.gg/5UBnR3P", "Python Atlanta", True), - ("discord.gg/ccyrDKv", "C#", True), -] - -domain_name_blacklist = [ - ("pornhub.com", None, False), - ("liveleak.com", None, False), - ("grabify.link", None, False), - ("bmwforum.co", None, False), - ("leancoding.co", None, False), - ("spottyfly.com", None, False), - ("stopify.co", None, False), - ("yoรผtu.be", None, False), - ("discรถrd.com", None, False), - ("minecrรคft.com", None, False), - ("freegiftcards.co", None, False), - ("disรงordapp.com", None, False), - ("fortnight.space", None, False), - ("fortnitechat.site", None, False), - ("joinmy.site", None, False), - ("curiouscat.club", None, False), - ("catsnthings.fun", None, False), - ("yourtube.site", None, False), - ("youtubeshort.watch", None, False), - ("catsnthing.com", None, False), - ("youtubeshort.pro", None, False), - ("canadianlumberjacks.online", None, False), - ("poweredbydialup.club", None, False), - ("poweredbydialup.online", None, False), - ("poweredbysecurity.org", None, False), - ("poweredbysecurity.online", None, False), - ("ssteam.site", None, False), - ("steamwalletgift.com", None, False), - ("discord.gift", None, False), - ("lmgtfy.com", None, False), -] - -filter_token_blacklist = [ - ("\bgoo+ks*\b", None, False), - ("\bky+s+\b", None, False), - ("\bki+ke+s*\b", None, False), - ("\bbeaner+s?\b", None, False), - ("\bcoo+ns*\b", None, False), - ("\bnig+lets*\b", None, False), - ("\bslant-eyes*\b", None, False), - ("\btowe?l-?head+s*\b", None, False), - ("\bchi*n+k+s*\b", None, False), - ("\bspick*s*\b", None, False), - ("\bkill* +(?:yo)?urself+\b", None, False), - ("\bjew+s*\b", None, False), - ("\bsuicide\b", None, False), - ("\brape\b", None, False), - ("\b(re+)tar+(d+|t+)(ed)?\b", None, False), - ("\bta+r+d+\b", None, False), - ("\bcunts*\b", None, False), - ("\btrann*y\b", None, False), - ("\bshemale\b", None, False), - ("fa+g+s*", None, False), - ("ๅ", None, False), - ("ๅ", None, False), - ("เฟ–", None, False), - ("เฟ•", None, False), - ("เฟ˜", None, False), - ("เฟ—", None, False), - ("cuck(?!oo+)", None, False), - ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), - ("fag+o+t+s*", None, False), -] - -file_format_whitelist = [ - (".3gp", None, True), - (".3g2", None, True), - (".avi", None, True), - (".bmp", None, True), - (".gif", None, True), - (".h264", None, True), - (".jpg", None, True), - (".jpeg", None, True), - (".m4v", None, True), - (".mkv", None, True), - (".mov", None, True), - (".mp4", None, True), - (".mpeg", None, True), - (".mpg", None, True), - (".png", None, True), - (".tiff", None, True), - (".wmv", None, True), - (".svg", None, True), - (".psd", "Photoshop", True), - (".ai", "Illustrator", True), - (".aep", "After Effects", True), - (".xcf", "GIMP", True), - (".mp3", None, True), - (".wav", None, True), - (".ogg", None, True), - (".webm", None, True), - (".webp", None, True), -] - -populate_data = { - "FILTER_TOKEN": filter_token_blacklist, - "DOMAIN_NAME": domain_name_blacklist, - "FILE_FORMAT": file_format_whitelist, - "GUILD_INVITE": guild_invite_whitelist, -} - - -class Migration(migrations.Migration): - dependencies = [("api", "0059_create_new_filterlist_model")] - - def populate_filterlists(app, _): - FilterList = app.get_model("api", "FilterList") - - for filterlist_type, metadata in populate_data.items(): - for content, comment, allowed in metadata: - FilterList.objects.create( - type=filterlist_type, - allowed=allowed, - content=content, - comment=comment, - ) - - def clear_filterlists(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.all().delete() - - operations = [ - migrations.RunPython(populate_filterlists, clear_filterlists) - ] diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py new file mode 100644 index 00000000..53846f02 --- /dev/null +++ b/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py @@ -0,0 +1,85 @@ +from django.db import migrations + +bad_guild_invite_whitelist = [ + ("discord.gg/python", "Python Discord", True), + ("discord.gg/4JJdJKb", "RLBot", True), + ("discord.gg/djPtTRJ", "Kivy", True), + ("discord.gg/QXyegWe", "Pyglet", True), + ("discord.gg/9XsucTT", "Panda3D", True), + ("discord.gg/AP3rq2k", "PyWeek", True), + ("discord.gg/vSPsP9t", "Microsoft Python", True), + ("discord.gg/bRCvFy9", "Discord.js Official", True), + ("discord.gg/9zT7NHP", "Programming Discussions", True), + ("discord.gg/ysd6M4r", "JetBrains Community", True), + ("discord.gg/4xJeCgy", "Raspberry Pie", True), + ("discord.gg/AStb3kZ", "Ren'Py", True), + ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), + ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), + ("discord.gg/jTtgWuy", "Django", True), + ("discord.gg/W9BypZF", "STEM", True), + ("discord.gg/dpy", "discord.py", True), + ("discord.gg/programming", "Programmers Hangout", True), + ("discord.gg/qhGUjGD", "SpeakJS", True), + ("discord.gg/eTbWSZj", "Functional Programming", True), + ("discord.gg/r8yreB6", "PyGame", True), + ("discord.gg/5UBnR3P", "Python Atlanta", True), + ("discord.gg/ccyrDKv", "C#", True), +] + +guild_invite_whitelist = [ + ("267624335836053506", "Python Discord", True), + ("348658686962696195", "RLBot", True), + ("423249981340778496", "Kivy", True), + ("438622377094414346", "Pyglet", True), + ("524691714909274162", "Panda3D", True), + ("666560367173828639", "PyWeek", True), + ("702724176489873509", "Microsoft Python", True), + ("222078108977594368", "Discord.js Official", True), + ("238666723824238602", "Programming Discussions", True), + ("433980600391696384", "JetBrains Community", True), + ("204621105720328193", "Raspberry Pie", True), + ("286633898581164032", "Ren'Py", True), + ("440186186024222721", "Python Discord: Emojis 1", True), + ("578587418123304970", "Python Discord: Emojis 2", True), + ("159039020565790721", "Django", True), + ("273944235143593984", "STEM", True), + ("336642139381301249", "discord.py", True), + ("244230771232079873", "Programmers Hangout", True), + ("239433591950540801", "SpeakJS", True), + ("280033776820813825", "Functional Programming", True), + ("349505959032389632", "PyGame", True), + ("488751051629920277", "Python Atlanta", True), + ("143867839282020352", "C#", True), +] + + +class Migration(migrations.Migration): + dependencies = [("api", "0059_populate_filterlists")] + + def fix_filterlist(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.filter(type="GUILD_INVITE").delete() # Clear out the stuff added in 0059. + + for content, comment, allowed in guild_invite_whitelist: + FilterList.objects.create( + type="GUILD_INVITE", + allowed=allowed, + content=content, + comment=comment, + ) + + def restore_bad_filterlist(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.filter(type="GUILD_INVITE").delete() + + for content, comment, allowed in bad_guild_invite_whitelist: + FilterList.objects.create( + type="GUILD_INVITE", + allowed=allowed, + content=content, + comment=comment, + ) + + operations = [ + migrations.RunPython(fix_filterlist, restore_bad_filterlist) + ] diff --git a/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py deleted file mode 100644 index eaaafb38..00000000 --- a/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py +++ /dev/null @@ -1,85 +0,0 @@ -from django.db import migrations - -bad_guild_invite_whitelist = [ - ("discord.gg/python", "Python Discord", True), - ("discord.gg/4JJdJKb", "RLBot", True), - ("discord.gg/djPtTRJ", "Kivy", True), - ("discord.gg/QXyegWe", "Pyglet", True), - ("discord.gg/9XsucTT", "Panda3D", True), - ("discord.gg/AP3rq2k", "PyWeek", True), - ("discord.gg/vSPsP9t", "Microsoft Python", True), - ("discord.gg/bRCvFy9", "Discord.js Official", True), - ("discord.gg/9zT7NHP", "Programming Discussions", True), - ("discord.gg/ysd6M4r", "JetBrains Community", True), - ("discord.gg/4xJeCgy", "Raspberry Pie", True), - ("discord.gg/AStb3kZ", "Ren'Py", True), - ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), - ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), - ("discord.gg/jTtgWuy", "Django", True), - ("discord.gg/W9BypZF", "STEM", True), - ("discord.gg/dpy", "discord.py", True), - ("discord.gg/programming", "Programmers Hangout", True), - ("discord.gg/qhGUjGD", "SpeakJS", True), - ("discord.gg/eTbWSZj", "Functional Programming", True), - ("discord.gg/r8yreB6", "PyGame", True), - ("discord.gg/5UBnR3P", "Python Atlanta", True), - ("discord.gg/ccyrDKv", "C#", True), -] - -guild_invite_whitelist = [ - ("267624335836053506", "Python Discord", True), - ("348658686962696195", "RLBot", True), - ("423249981340778496", "Kivy", True), - ("438622377094414346", "Pyglet", True), - ("524691714909274162", "Panda3D", True), - ("666560367173828639", "PyWeek", True), - ("702724176489873509", "Microsoft Python", True), - ("222078108977594368", "Discord.js Official", True), - ("238666723824238602", "Programming Discussions", True), - ("433980600391696384", "JetBrains Community", True), - ("204621105720328193", "Raspberry Pie", True), - ("286633898581164032", "Ren'Py", True), - ("440186186024222721", "Python Discord: Emojis 1", True), - ("578587418123304970", "Python Discord: Emojis 2", True), - ("159039020565790721", "Django", True), - ("273944235143593984", "STEM", True), - ("336642139381301249", "discord.py", True), - ("244230771232079873", "Programmers Hangout", True), - ("239433591950540801", "SpeakJS", True), - ("280033776820813825", "Functional Programming", True), - ("349505959032389632", "PyGame", True), - ("488751051629920277", "Python Atlanta", True), - ("143867839282020352", "C#", True), -] - - -class Migration(migrations.Migration): - dependencies = [("api", "0060_populate_filterlists")] - - def fix_filterlist(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.filter(type="GUILD_INVITE").delete() # Clear out the stuff added in 0059. - - for content, comment, allowed in guild_invite_whitelist: - FilterList.objects.create( - type="GUILD_INVITE", - allowed=allowed, - content=content, - comment=comment, - ) - - def restore_bad_filterlist(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.filter(type="GUILD_INVITE").delete() - - for content, comment, allowed in bad_guild_invite_whitelist: - FilterList.objects.create( - type="GUILD_INVITE", - allowed=allowed, - content=content, - comment=comment, - ) - - operations = [ - migrations.RunPython(fix_filterlist, restore_bad_filterlist) - ] diff --git a/pydis_site/apps/api/models/bot/off_topic_channel_name.py b/pydis_site/apps/api/models/bot/off_topic_channel_name.py index 403c7465..20e77b9f 100644 --- a/pydis_site/apps/api/models/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/models/bot/off_topic_channel_name.py @@ -16,11 +16,6 @@ class OffTopicChannelName(ModelReprMixin, models.Model): help_text="The actual channel name that will be used on our Discord server." ) - used = models.BooleanField( - default=False, - help_text="Whether or not this name has already been used during this rotation", - ) - def __str__(self): """Returns the current off-topic name, for display purposes.""" return self.name diff --git a/pydis_site/apps/api/tests/test_off_topic_channel_names.py b/pydis_site/apps/api/tests/test_off_topic_channel_names.py index 3ab8b22d..bd42cd81 100644 --- a/pydis_site/apps/api/tests/test_off_topic_channel_names.py +++ b/pydis_site/apps/api/tests/test_off_topic_channel_names.py @@ -10,14 +10,12 @@ class UnauthenticatedTests(APISubdomainTestCase): self.client.force_authenticate(user=None) def test_cannot_read_off_topic_channel_name_list(self): - """Return a 401 response when not authenticated.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(url) self.assertEqual(response.status_code, 401) def test_cannot_read_off_topic_channel_name_list_with_random_item_param(self): - """Return a 401 response when `random_items` provided and not authenticated.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=no') @@ -26,7 +24,6 @@ class UnauthenticatedTests(APISubdomainTestCase): class EmptyDatabaseTests(APISubdomainTestCase): def test_returns_empty_object(self): - """Return empty list when no names in database.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(url) @@ -34,7 +31,6 @@ class EmptyDatabaseTests(APISubdomainTestCase): self.assertEqual(response.json(), []) def test_returns_empty_list_with_get_all_param(self): - """Return empty list when no names and `random_items` param provided.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=5') @@ -42,7 +38,6 @@ class EmptyDatabaseTests(APISubdomainTestCase): self.assertEqual(response.json(), []) def test_returns_400_for_bad_random_items_param(self): - """Return error message when passing not integer as `random_items`.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=totally-a-valid-integer') @@ -52,7 +47,6 @@ class EmptyDatabaseTests(APISubdomainTestCase): }) def test_returns_400_for_negative_random_items_param(self): - """Return error message when passing negative int as `random_items`.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=-5') @@ -65,11 +59,10 @@ class EmptyDatabaseTests(APISubdomainTestCase): class ListTests(APISubdomainTestCase): @classmethod def setUpTestData(cls): - cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand', used=False) - cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk', used=True) + cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand') + cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk') def test_returns_name_in_list(self): - """Return all off-topic channel names.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(url) @@ -83,21 +76,11 @@ class ListTests(APISubdomainTestCase): ) def test_returns_single_item_with_random_items_param_set_to_1(self): - """Return not-used name instead used.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=1') self.assertEqual(response.status_code, 200) self.assertEqual(len(response.json()), 1) - self.assertEqual(response.json(), [self.test_name.name]) - - def test_running_out_of_names_with_random_parameter(self): - """Reset names `used` parameter to `False` when running out of names.""" - url = reverse('bot:offtopicchannelname-list', host='api') - response = self.client.get(f'{url}?random_items=2') - - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), [self.test_name.name, self.test_name_2.name]) class CreationTests(APISubdomainTestCase): @@ -110,7 +93,6 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response.status_code, 201) def test_returns_201_for_unicode_chars(self): - """Accept all valid characters.""" url = reverse('bot:offtopicchannelname-list', host='api') names = ( '๐– ๐–ก๐–ข๐–ฃ๐–ค๐–ฅ๐–ฆ๐–ง๐–จ๐–ฉ๐–ช๐–ซ๐–ฌ๐–ญ๐–ฎ๐–ฏ๐–ฐ๐–ฑ๐–ฒ๐–ณ๐–ด๐–ต๐–ถ๐–ท๐–ธ๐–น', @@ -122,7 +104,6 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response.status_code, 201) def test_returns_400_for_missing_name_param(self): - """Return error message when name not provided.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.post(url) self.assertEqual(response.status_code, 400) @@ -131,7 +112,6 @@ class CreationTests(APISubdomainTestCase): }) def test_returns_400_for_bad_name_param(self): - """Return error message when invalid characters provided.""" url = reverse('bot:offtopicchannelname-list', host='api') invalid_names = ( 'space between words', @@ -154,21 +134,18 @@ class DeletionTests(APISubdomainTestCase): cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk') def test_deleting_unknown_name_returns_404(self): - """Return 404 reponse when trying to delete unknown name.""" url = reverse('bot:offtopicchannelname-detail', args=('unknown-name',), host='api') response = self.client.delete(url) self.assertEqual(response.status_code, 404) def test_deleting_known_name_returns_204(self): - """Return 204 response when deleting was successful.""" url = reverse('bot:offtopicchannelname-detail', args=(self.test_name.name,), host='api') response = self.client.delete(url) self.assertEqual(response.status_code, 204) def test_name_gets_deleted(self): - """Name gets actually deleted.""" url = reverse('bot:offtopicchannelname-detail', args=(self.test_name_2.name,), host='api') response = self.client.delete(url) diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py index 826ad25e..d6da2399 100644 --- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py @@ -1,4 +1,3 @@ -from django.db.models import Case, Value, When from django.db.models.query import QuerySet from django.http.request import HttpRequest from django.shortcuts import get_object_or_404 @@ -21,9 +20,7 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): 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 - that is not used in current rotation. - When running out of names, API will mark all names to not used and start new rotation. + ... then the API will return `5` random items from the database. #### Response format Return a list of off-topic-channel names: @@ -109,27 +106,7 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): 'random_items': ["Must be a positive integer."] }) - queryset = self.get_queryset().order_by('used', '?')[:random_count] - - # When any name is used in our listing then this means we reached end of round - # and we need to reset all other names `used` to False - if any(offtopic_name.used for offtopic_name in queryset): - # These names that we just got have to be excluded from updating used to False - self.get_queryset().update( - used=Case( - When( - name__in=(offtopic_name.name for offtopic_name in queryset), - then=Value(True) - ), - default=Value(False) - ) - ) - else: - # Otherwise mark selected names `used` to True - self.get_queryset().filter( - name__in=(offtopic_name.name for offtopic_name in queryset) - ).update(used=True) - + queryset = self.get_queryset().order_by('?')[:random_count] serialized = self.serializer_class(queryset, many=True) return Response(serialized.data) -- cgit v1.2.3 From c1ec2f1f9fe443cdde3750b1df1c3d969df3e67e Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 30 Aug 2020 08:20:25 +0300 Subject: Revert "Revert pull request #348" This reverts commit dae68c46831ef14e6a0715add1025f9af1b5a73d. --- .../api/migrations/0051_create_news_setting.py | 25 ---- .../api/migrations/0052_create_news_setting.py | 25 ++++ .../api/migrations/0052_remove_user_avatar_hash.py | 17 --- .../migrations/0053_offtopicchannelname_used.py | 18 +++ .../api/migrations/0053_user_roles_to_array.py | 24 ---- .../api/migrations/0054_remove_user_avatar_hash.py | 17 +++ .../0054_user_invalidate_unknown_role.py | 21 --- .../api/migrations/0055_merge_20200714_2027.py | 14 -- .../apps/api/migrations/0055_reminder_mentions.py | 20 --- .../api/migrations/0055_user_roles_to_array.py | 24 ++++ .../api/migrations/0056_allow_blank_user_roles.py | 21 --- .../0056_user_invalidate_unknown_role.py | 21 +++ .../api/migrations/0057_merge_20200716_0751.py | 14 -- .../apps/api/migrations/0057_reminder_mentions.py | 20 +++ .../api/migrations/0058_allow_blank_user_roles.py | 21 +++ .../migrations/0058_create_new_filterlist_model.py | 33 ----- .../migrations/0059_create_new_filterlist_model.py | 33 +++++ .../api/migrations/0059_populate_filterlists.py | 153 --------------------- .../api/migrations/0060_populate_filterlists.py | 153 +++++++++++++++++++++ .../migrations/0060_populate_filterlists_fix.py | 85 ------------ .../migrations/0061_populate_filterlists_fix.py | 85 ++++++++++++ .../apps/api/models/bot/off_topic_channel_name.py | 5 + .../apps/api/tests/test_off_topic_channel_names.py | 27 +++- .../api/viewsets/bot/off_topic_channel_name.py | 27 +++- 24 files changed, 472 insertions(+), 431 deletions(-) delete mode 100644 pydis_site/apps/api/migrations/0051_create_news_setting.py create mode 100644 pydis_site/apps/api/migrations/0052_create_news_setting.py delete mode 100644 pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py create mode 100644 pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py delete mode 100644 pydis_site/apps/api/migrations/0053_user_roles_to_array.py create mode 100644 pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py delete mode 100644 pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py delete mode 100644 pydis_site/apps/api/migrations/0055_merge_20200714_2027.py delete mode 100644 pydis_site/apps/api/migrations/0055_reminder_mentions.py create mode 100644 pydis_site/apps/api/migrations/0055_user_roles_to_array.py delete mode 100644 pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py create mode 100644 pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py delete mode 100644 pydis_site/apps/api/migrations/0057_merge_20200716_0751.py create mode 100644 pydis_site/apps/api/migrations/0057_reminder_mentions.py create mode 100644 pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py delete mode 100644 pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py create mode 100644 pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py delete mode 100644 pydis_site/apps/api/migrations/0059_populate_filterlists.py create mode 100644 pydis_site/apps/api/migrations/0060_populate_filterlists.py delete mode 100644 pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py create mode 100644 pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py diff --git a/pydis_site/apps/api/migrations/0051_create_news_setting.py b/pydis_site/apps/api/migrations/0051_create_news_setting.py deleted file mode 100644 index f18fdfb1..00000000 --- a/pydis_site/apps/api/migrations/0051_create_news_setting.py +++ /dev/null @@ -1,25 +0,0 @@ -from django.db import migrations - - -def up(apps, schema_editor): - BotSetting = apps.get_model('api', 'BotSetting') - setting = BotSetting( - name='news', - data={} - ).save() - - -def down(apps, schema_editor): - BotSetting = apps.get_model('api', 'BotSetting') - BotSetting.objects.get(name='news').delete() - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0050_remove_infractions_active_default_value'), - ] - - operations = [ - migrations.RunPython(up, down) - ] diff --git a/pydis_site/apps/api/migrations/0052_create_news_setting.py b/pydis_site/apps/api/migrations/0052_create_news_setting.py new file mode 100644 index 00000000..b101d19d --- /dev/null +++ b/pydis_site/apps/api/migrations/0052_create_news_setting.py @@ -0,0 +1,25 @@ +from django.db import migrations + + +def up(apps, schema_editor): + BotSetting = apps.get_model('api', 'BotSetting') + setting = BotSetting( + name='news', + data={} + ).save() + + +def down(apps, schema_editor): + BotSetting = apps.get_model('api', 'BotSetting') + BotSetting.objects.get(name='news').delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0051_allow_blank_message_embeds'), + ] + + operations = [ + migrations.RunPython(up, down) + ] diff --git a/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py deleted file mode 100644 index 26b3b954..00000000 --- a/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.2.11 on 2020-05-27 07:17 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0051_create_news_setting'), - ] - - operations = [ - migrations.RemoveField( - model_name='user', - name='avatar_hash', - ), - ] diff --git a/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py new file mode 100644 index 00000000..b51ce1d2 --- /dev/null +++ b/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.11 on 2020-03-30 10:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0052_create_news_setting'), + ] + + operations = [ + migrations.AddField( + model_name='offtopicchannelname', + name='used', + field=models.BooleanField(default=False, help_text='Whether or not this name has already been used during this rotation'), + ), + ] diff --git a/pydis_site/apps/api/migrations/0053_user_roles_to_array.py b/pydis_site/apps/api/migrations/0053_user_roles_to_array.py deleted file mode 100644 index 7ff3a548..00000000 --- a/pydis_site/apps/api/migrations/0053_user_roles_to_array.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.2.11 on 2020-06-02 13:42 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0052_remove_user_avatar_hash'), - ] - - operations = [ - migrations.RemoveField( - model_name='user', - name='roles', - ), - migrations.AddField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py new file mode 100644 index 00000000..be9fd948 --- /dev/null +++ b/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.11 on 2020-05-27 07:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0053_offtopicchannelname_used'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='avatar_hash', + ), + ] diff --git a/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py deleted file mode 100644 index 96230015..00000000 --- a/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 2.2.11 on 2020-06-02 20:08 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models -import pydis_site.apps.api.models.bot.user - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0053_user_roles_to_array'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py b/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py deleted file mode 100644 index f2a0e638..00000000 --- a/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-14 20:27 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0051_allow_blank_message_embeds'), - ('api', '0054_user_invalidate_unknown_role'), - ] - - operations = [ - ] diff --git a/pydis_site/apps/api/migrations/0055_reminder_mentions.py b/pydis_site/apps/api/migrations/0055_reminder_mentions.py deleted file mode 100644 index d73b450d..00000000 --- a/pydis_site/apps/api/migrations/0055_reminder_mentions.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.2.14 on 2020-07-15 07:37 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0054_user_invalidate_unknown_role'), - ] - - operations = [ - migrations.AddField( - model_name='reminder', - name='mentions', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0055_user_roles_to_array.py b/pydis_site/apps/api/migrations/0055_user_roles_to_array.py new file mode 100644 index 00000000..e7b4a983 --- /dev/null +++ b/pydis_site/apps/api/migrations/0055_user_roles_to_array.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.11 on 2020-06-02 13:42 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0054_remove_user_avatar_hash'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='roles', + ), + migrations.AddField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py deleted file mode 100644 index 489941c7..00000000 --- a/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-14 20:35 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models -import pydis_site.apps.api.models.bot.user - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0055_merge_20200714_2027'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py new file mode 100644 index 00000000..ab2696aa --- /dev/null +++ b/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.11 on 2020-06-02 20:08 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.user + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0055_user_roles_to_array'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py b/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py deleted file mode 100644 index 47a6d2d4..00000000 --- a/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 2.2.14 on 2020-07-16 07:51 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0055_reminder_mentions'), - ('api', '0056_allow_blank_user_roles'), - ] - - operations = [ - ] diff --git a/pydis_site/apps/api/migrations/0057_reminder_mentions.py b/pydis_site/apps/api/migrations/0057_reminder_mentions.py new file mode 100644 index 00000000..fb829a17 --- /dev/null +++ b/pydis_site/apps/api/migrations/0057_reminder_mentions.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.14 on 2020-07-15 07:37 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0056_user_invalidate_unknown_role'), + ] + + operations = [ + migrations.AddField( + model_name='reminder', + name='mentions', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py new file mode 100644 index 00000000..8f7fddfc --- /dev/null +++ b/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0.8 on 2020-07-14 20:35 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.user + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0057_reminder_mentions'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py deleted file mode 100644 index aecfdad7..00000000 --- a/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-15 11:23 - -from django.db import migrations, models -import pydis_site.apps.api.models.mixins - - -class Migration(migrations.Migration): - dependencies = [ - ('api', '0057_merge_20200716_0751'), - ] - - operations = [ - migrations.CreateModel( - name='FilterList', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('type', models.CharField( - choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), - ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], - help_text='The type of allowlist this is on.', max_length=50)), - ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), - ('content', models.TextField(help_text='The data to add to the allow or denylist.')), - ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), - ], - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - migrations.AddConstraint( - model_name='filterlist', - constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list') - ) - ] diff --git a/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py new file mode 100644 index 00000000..eac5542d --- /dev/null +++ b/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py @@ -0,0 +1,33 @@ +# Generated by Django 3.0.8 on 2020-07-15 11:23 + +from django.db import migrations, models +import pydis_site.apps.api.models.mixins + + +class Migration(migrations.Migration): + dependencies = [ + ('api', '0058_allow_blank_user_roles'), + ] + + operations = [ + migrations.CreateModel( + name='FilterList', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('type', models.CharField( + choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), + ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], + help_text='The type of allowlist this is on.', max_length=50)), + ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), + ('content', models.TextField(help_text='The data to add to the allow or denylist.')), + ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), + ], + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), + ), + migrations.AddConstraint( + model_name='filterlist', + constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list') + ) + ] diff --git a/pydis_site/apps/api/migrations/0059_populate_filterlists.py b/pydis_site/apps/api/migrations/0059_populate_filterlists.py deleted file mode 100644 index 8c550191..00000000 --- a/pydis_site/apps/api/migrations/0059_populate_filterlists.py +++ /dev/null @@ -1,153 +0,0 @@ -from django.db import migrations - -guild_invite_whitelist = [ - ("discord.gg/python", "Python Discord", True), - ("discord.gg/4JJdJKb", "RLBot", True), - ("discord.gg/djPtTRJ", "Kivy", True), - ("discord.gg/QXyegWe", "Pyglet", True), - ("discord.gg/9XsucTT", "Panda3D", True), - ("discord.gg/AP3rq2k", "PyWeek", True), - ("discord.gg/vSPsP9t", "Microsoft Python", True), - ("discord.gg/bRCvFy9", "Discord.js Official", True), - ("discord.gg/9zT7NHP", "Programming Discussions", True), - ("discord.gg/ysd6M4r", "JetBrains Community", True), - ("discord.gg/4xJeCgy", "Raspberry Pie", True), - ("discord.gg/AStb3kZ", "Ren'Py", True), - ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), - ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), - ("discord.gg/jTtgWuy", "Django", True), - ("discord.gg/W9BypZF", "STEM", True), - ("discord.gg/dpy", "discord.py", True), - ("discord.gg/programming", "Programmers Hangout", True), - ("discord.gg/qhGUjGD", "SpeakJS", True), - ("discord.gg/eTbWSZj", "Functional Programming", True), - ("discord.gg/r8yreB6", "PyGame", True), - ("discord.gg/5UBnR3P", "Python Atlanta", True), - ("discord.gg/ccyrDKv", "C#", True), -] - -domain_name_blacklist = [ - ("pornhub.com", None, False), - ("liveleak.com", None, False), - ("grabify.link", None, False), - ("bmwforum.co", None, False), - ("leancoding.co", None, False), - ("spottyfly.com", None, False), - ("stopify.co", None, False), - ("yoรผtu.be", None, False), - ("discรถrd.com", None, False), - ("minecrรคft.com", None, False), - ("freegiftcards.co", None, False), - ("disรงordapp.com", None, False), - ("fortnight.space", None, False), - ("fortnitechat.site", None, False), - ("joinmy.site", None, False), - ("curiouscat.club", None, False), - ("catsnthings.fun", None, False), - ("yourtube.site", None, False), - ("youtubeshort.watch", None, False), - ("catsnthing.com", None, False), - ("youtubeshort.pro", None, False), - ("canadianlumberjacks.online", None, False), - ("poweredbydialup.club", None, False), - ("poweredbydialup.online", None, False), - ("poweredbysecurity.org", None, False), - ("poweredbysecurity.online", None, False), - ("ssteam.site", None, False), - ("steamwalletgift.com", None, False), - ("discord.gift", None, False), - ("lmgtfy.com", None, False), -] - -filter_token_blacklist = [ - ("\bgoo+ks*\b", None, False), - ("\bky+s+\b", None, False), - ("\bki+ke+s*\b", None, False), - ("\bbeaner+s?\b", None, False), - ("\bcoo+ns*\b", None, False), - ("\bnig+lets*\b", None, False), - ("\bslant-eyes*\b", None, False), - ("\btowe?l-?head+s*\b", None, False), - ("\bchi*n+k+s*\b", None, False), - ("\bspick*s*\b", None, False), - ("\bkill* +(?:yo)?urself+\b", None, False), - ("\bjew+s*\b", None, False), - ("\bsuicide\b", None, False), - ("\brape\b", None, False), - ("\b(re+)tar+(d+|t+)(ed)?\b", None, False), - ("\bta+r+d+\b", None, False), - ("\bcunts*\b", None, False), - ("\btrann*y\b", None, False), - ("\bshemale\b", None, False), - ("fa+g+s*", None, False), - ("ๅ", None, False), - ("ๅ", None, False), - ("เฟ–", None, False), - ("เฟ•", None, False), - ("เฟ˜", None, False), - ("เฟ—", None, False), - ("cuck(?!oo+)", None, False), - ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), - ("fag+o+t+s*", None, False), -] - -file_format_whitelist = [ - (".3gp", None, True), - (".3g2", None, True), - (".avi", None, True), - (".bmp", None, True), - (".gif", None, True), - (".h264", None, True), - (".jpg", None, True), - (".jpeg", None, True), - (".m4v", None, True), - (".mkv", None, True), - (".mov", None, True), - (".mp4", None, True), - (".mpeg", None, True), - (".mpg", None, True), - (".png", None, True), - (".tiff", None, True), - (".wmv", None, True), - (".svg", None, True), - (".psd", "Photoshop", True), - (".ai", "Illustrator", True), - (".aep", "After Effects", True), - (".xcf", "GIMP", True), - (".mp3", None, True), - (".wav", None, True), - (".ogg", None, True), - (".webm", None, True), - (".webp", None, True), -] - -populate_data = { - "FILTER_TOKEN": filter_token_blacklist, - "DOMAIN_NAME": domain_name_blacklist, - "FILE_FORMAT": file_format_whitelist, - "GUILD_INVITE": guild_invite_whitelist, -} - - -class Migration(migrations.Migration): - dependencies = [("api", "0058_create_new_filterlist_model")] - - def populate_filterlists(app, _): - FilterList = app.get_model("api", "FilterList") - - for filterlist_type, metadata in populate_data.items(): - for content, comment, allowed in metadata: - FilterList.objects.create( - type=filterlist_type, - allowed=allowed, - content=content, - comment=comment, - ) - - def clear_filterlists(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.all().delete() - - operations = [ - migrations.RunPython(populate_filterlists, clear_filterlists) - ] diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists.py b/pydis_site/apps/api/migrations/0060_populate_filterlists.py new file mode 100644 index 00000000..35fde95a --- /dev/null +++ b/pydis_site/apps/api/migrations/0060_populate_filterlists.py @@ -0,0 +1,153 @@ +from django.db import migrations + +guild_invite_whitelist = [ + ("discord.gg/python", "Python Discord", True), + ("discord.gg/4JJdJKb", "RLBot", True), + ("discord.gg/djPtTRJ", "Kivy", True), + ("discord.gg/QXyegWe", "Pyglet", True), + ("discord.gg/9XsucTT", "Panda3D", True), + ("discord.gg/AP3rq2k", "PyWeek", True), + ("discord.gg/vSPsP9t", "Microsoft Python", True), + ("discord.gg/bRCvFy9", "Discord.js Official", True), + ("discord.gg/9zT7NHP", "Programming Discussions", True), + ("discord.gg/ysd6M4r", "JetBrains Community", True), + ("discord.gg/4xJeCgy", "Raspberry Pie", True), + ("discord.gg/AStb3kZ", "Ren'Py", True), + ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), + ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), + ("discord.gg/jTtgWuy", "Django", True), + ("discord.gg/W9BypZF", "STEM", True), + ("discord.gg/dpy", "discord.py", True), + ("discord.gg/programming", "Programmers Hangout", True), + ("discord.gg/qhGUjGD", "SpeakJS", True), + ("discord.gg/eTbWSZj", "Functional Programming", True), + ("discord.gg/r8yreB6", "PyGame", True), + ("discord.gg/5UBnR3P", "Python Atlanta", True), + ("discord.gg/ccyrDKv", "C#", True), +] + +domain_name_blacklist = [ + ("pornhub.com", None, False), + ("liveleak.com", None, False), + ("grabify.link", None, False), + ("bmwforum.co", None, False), + ("leancoding.co", None, False), + ("spottyfly.com", None, False), + ("stopify.co", None, False), + ("yoรผtu.be", None, False), + ("discรถrd.com", None, False), + ("minecrรคft.com", None, False), + ("freegiftcards.co", None, False), + ("disรงordapp.com", None, False), + ("fortnight.space", None, False), + ("fortnitechat.site", None, False), + ("joinmy.site", None, False), + ("curiouscat.club", None, False), + ("catsnthings.fun", None, False), + ("yourtube.site", None, False), + ("youtubeshort.watch", None, False), + ("catsnthing.com", None, False), + ("youtubeshort.pro", None, False), + ("canadianlumberjacks.online", None, False), + ("poweredbydialup.club", None, False), + ("poweredbydialup.online", None, False), + ("poweredbysecurity.org", None, False), + ("poweredbysecurity.online", None, False), + ("ssteam.site", None, False), + ("steamwalletgift.com", None, False), + ("discord.gift", None, False), + ("lmgtfy.com", None, False), +] + +filter_token_blacklist = [ + ("\bgoo+ks*\b", None, False), + ("\bky+s+\b", None, False), + ("\bki+ke+s*\b", None, False), + ("\bbeaner+s?\b", None, False), + ("\bcoo+ns*\b", None, False), + ("\bnig+lets*\b", None, False), + ("\bslant-eyes*\b", None, False), + ("\btowe?l-?head+s*\b", None, False), + ("\bchi*n+k+s*\b", None, False), + ("\bspick*s*\b", None, False), + ("\bkill* +(?:yo)?urself+\b", None, False), + ("\bjew+s*\b", None, False), + ("\bsuicide\b", None, False), + ("\brape\b", None, False), + ("\b(re+)tar+(d+|t+)(ed)?\b", None, False), + ("\bta+r+d+\b", None, False), + ("\bcunts*\b", None, False), + ("\btrann*y\b", None, False), + ("\bshemale\b", None, False), + ("fa+g+s*", None, False), + ("ๅ", None, False), + ("ๅ", None, False), + ("เฟ–", None, False), + ("เฟ•", None, False), + ("เฟ˜", None, False), + ("เฟ—", None, False), + ("cuck(?!oo+)", None, False), + ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), + ("fag+o+t+s*", None, False), +] + +file_format_whitelist = [ + (".3gp", None, True), + (".3g2", None, True), + (".avi", None, True), + (".bmp", None, True), + (".gif", None, True), + (".h264", None, True), + (".jpg", None, True), + (".jpeg", None, True), + (".m4v", None, True), + (".mkv", None, True), + (".mov", None, True), + (".mp4", None, True), + (".mpeg", None, True), + (".mpg", None, True), + (".png", None, True), + (".tiff", None, True), + (".wmv", None, True), + (".svg", None, True), + (".psd", "Photoshop", True), + (".ai", "Illustrator", True), + (".aep", "After Effects", True), + (".xcf", "GIMP", True), + (".mp3", None, True), + (".wav", None, True), + (".ogg", None, True), + (".webm", None, True), + (".webp", None, True), +] + +populate_data = { + "FILTER_TOKEN": filter_token_blacklist, + "DOMAIN_NAME": domain_name_blacklist, + "FILE_FORMAT": file_format_whitelist, + "GUILD_INVITE": guild_invite_whitelist, +} + + +class Migration(migrations.Migration): + dependencies = [("api", "0059_create_new_filterlist_model")] + + def populate_filterlists(app, _): + FilterList = app.get_model("api", "FilterList") + + for filterlist_type, metadata in populate_data.items(): + for content, comment, allowed in metadata: + FilterList.objects.create( + type=filterlist_type, + allowed=allowed, + content=content, + comment=comment, + ) + + def clear_filterlists(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.all().delete() + + operations = [ + migrations.RunPython(populate_filterlists, clear_filterlists) + ] diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py deleted file mode 100644 index 53846f02..00000000 --- a/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py +++ /dev/null @@ -1,85 +0,0 @@ -from django.db import migrations - -bad_guild_invite_whitelist = [ - ("discord.gg/python", "Python Discord", True), - ("discord.gg/4JJdJKb", "RLBot", True), - ("discord.gg/djPtTRJ", "Kivy", True), - ("discord.gg/QXyegWe", "Pyglet", True), - ("discord.gg/9XsucTT", "Panda3D", True), - ("discord.gg/AP3rq2k", "PyWeek", True), - ("discord.gg/vSPsP9t", "Microsoft Python", True), - ("discord.gg/bRCvFy9", "Discord.js Official", True), - ("discord.gg/9zT7NHP", "Programming Discussions", True), - ("discord.gg/ysd6M4r", "JetBrains Community", True), - ("discord.gg/4xJeCgy", "Raspberry Pie", True), - ("discord.gg/AStb3kZ", "Ren'Py", True), - ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), - ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), - ("discord.gg/jTtgWuy", "Django", True), - ("discord.gg/W9BypZF", "STEM", True), - ("discord.gg/dpy", "discord.py", True), - ("discord.gg/programming", "Programmers Hangout", True), - ("discord.gg/qhGUjGD", "SpeakJS", True), - ("discord.gg/eTbWSZj", "Functional Programming", True), - ("discord.gg/r8yreB6", "PyGame", True), - ("discord.gg/5UBnR3P", "Python Atlanta", True), - ("discord.gg/ccyrDKv", "C#", True), -] - -guild_invite_whitelist = [ - ("267624335836053506", "Python Discord", True), - ("348658686962696195", "RLBot", True), - ("423249981340778496", "Kivy", True), - ("438622377094414346", "Pyglet", True), - ("524691714909274162", "Panda3D", True), - ("666560367173828639", "PyWeek", True), - ("702724176489873509", "Microsoft Python", True), - ("222078108977594368", "Discord.js Official", True), - ("238666723824238602", "Programming Discussions", True), - ("433980600391696384", "JetBrains Community", True), - ("204621105720328193", "Raspberry Pie", True), - ("286633898581164032", "Ren'Py", True), - ("440186186024222721", "Python Discord: Emojis 1", True), - ("578587418123304970", "Python Discord: Emojis 2", True), - ("159039020565790721", "Django", True), - ("273944235143593984", "STEM", True), - ("336642139381301249", "discord.py", True), - ("244230771232079873", "Programmers Hangout", True), - ("239433591950540801", "SpeakJS", True), - ("280033776820813825", "Functional Programming", True), - ("349505959032389632", "PyGame", True), - ("488751051629920277", "Python Atlanta", True), - ("143867839282020352", "C#", True), -] - - -class Migration(migrations.Migration): - dependencies = [("api", "0059_populate_filterlists")] - - def fix_filterlist(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.filter(type="GUILD_INVITE").delete() # Clear out the stuff added in 0059. - - for content, comment, allowed in guild_invite_whitelist: - FilterList.objects.create( - type="GUILD_INVITE", - allowed=allowed, - content=content, - comment=comment, - ) - - def restore_bad_filterlist(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.filter(type="GUILD_INVITE").delete() - - for content, comment, allowed in bad_guild_invite_whitelist: - FilterList.objects.create( - type="GUILD_INVITE", - allowed=allowed, - content=content, - comment=comment, - ) - - operations = [ - migrations.RunPython(fix_filterlist, restore_bad_filterlist) - ] diff --git a/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py new file mode 100644 index 00000000..eaaafb38 --- /dev/null +++ b/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py @@ -0,0 +1,85 @@ +from django.db import migrations + +bad_guild_invite_whitelist = [ + ("discord.gg/python", "Python Discord", True), + ("discord.gg/4JJdJKb", "RLBot", True), + ("discord.gg/djPtTRJ", "Kivy", True), + ("discord.gg/QXyegWe", "Pyglet", True), + ("discord.gg/9XsucTT", "Panda3D", True), + ("discord.gg/AP3rq2k", "PyWeek", True), + ("discord.gg/vSPsP9t", "Microsoft Python", True), + ("discord.gg/bRCvFy9", "Discord.js Official", True), + ("discord.gg/9zT7NHP", "Programming Discussions", True), + ("discord.gg/ysd6M4r", "JetBrains Community", True), + ("discord.gg/4xJeCgy", "Raspberry Pie", True), + ("discord.gg/AStb3kZ", "Ren'Py", True), + ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), + ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), + ("discord.gg/jTtgWuy", "Django", True), + ("discord.gg/W9BypZF", "STEM", True), + ("discord.gg/dpy", "discord.py", True), + ("discord.gg/programming", "Programmers Hangout", True), + ("discord.gg/qhGUjGD", "SpeakJS", True), + ("discord.gg/eTbWSZj", "Functional Programming", True), + ("discord.gg/r8yreB6", "PyGame", True), + ("discord.gg/5UBnR3P", "Python Atlanta", True), + ("discord.gg/ccyrDKv", "C#", True), +] + +guild_invite_whitelist = [ + ("267624335836053506", "Python Discord", True), + ("348658686962696195", "RLBot", True), + ("423249981340778496", "Kivy", True), + ("438622377094414346", "Pyglet", True), + ("524691714909274162", "Panda3D", True), + ("666560367173828639", "PyWeek", True), + ("702724176489873509", "Microsoft Python", True), + ("222078108977594368", "Discord.js Official", True), + ("238666723824238602", "Programming Discussions", True), + ("433980600391696384", "JetBrains Community", True), + ("204621105720328193", "Raspberry Pie", True), + ("286633898581164032", "Ren'Py", True), + ("440186186024222721", "Python Discord: Emojis 1", True), + ("578587418123304970", "Python Discord: Emojis 2", True), + ("159039020565790721", "Django", True), + ("273944235143593984", "STEM", True), + ("336642139381301249", "discord.py", True), + ("244230771232079873", "Programmers Hangout", True), + ("239433591950540801", "SpeakJS", True), + ("280033776820813825", "Functional Programming", True), + ("349505959032389632", "PyGame", True), + ("488751051629920277", "Python Atlanta", True), + ("143867839282020352", "C#", True), +] + + +class Migration(migrations.Migration): + dependencies = [("api", "0060_populate_filterlists")] + + def fix_filterlist(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.filter(type="GUILD_INVITE").delete() # Clear out the stuff added in 0059. + + for content, comment, allowed in guild_invite_whitelist: + FilterList.objects.create( + type="GUILD_INVITE", + allowed=allowed, + content=content, + comment=comment, + ) + + def restore_bad_filterlist(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.filter(type="GUILD_INVITE").delete() + + for content, comment, allowed in bad_guild_invite_whitelist: + FilterList.objects.create( + type="GUILD_INVITE", + allowed=allowed, + content=content, + comment=comment, + ) + + operations = [ + migrations.RunPython(fix_filterlist, restore_bad_filterlist) + ] diff --git a/pydis_site/apps/api/models/bot/off_topic_channel_name.py b/pydis_site/apps/api/models/bot/off_topic_channel_name.py index 20e77b9f..403c7465 100644 --- a/pydis_site/apps/api/models/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/models/bot/off_topic_channel_name.py @@ -16,6 +16,11 @@ class OffTopicChannelName(ModelReprMixin, models.Model): help_text="The actual channel name that will be used on our Discord server." ) + used = models.BooleanField( + default=False, + help_text="Whether or not this name has already been used during this rotation", + ) + def __str__(self): """Returns the current off-topic name, for display purposes.""" return self.name diff --git a/pydis_site/apps/api/tests/test_off_topic_channel_names.py b/pydis_site/apps/api/tests/test_off_topic_channel_names.py index bd42cd81..3ab8b22d 100644 --- a/pydis_site/apps/api/tests/test_off_topic_channel_names.py +++ b/pydis_site/apps/api/tests/test_off_topic_channel_names.py @@ -10,12 +10,14 @@ class UnauthenticatedTests(APISubdomainTestCase): self.client.force_authenticate(user=None) def test_cannot_read_off_topic_channel_name_list(self): + """Return a 401 response when not authenticated.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(url) self.assertEqual(response.status_code, 401) def test_cannot_read_off_topic_channel_name_list_with_random_item_param(self): + """Return a 401 response when `random_items` provided and not authenticated.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=no') @@ -24,6 +26,7 @@ class UnauthenticatedTests(APISubdomainTestCase): class EmptyDatabaseTests(APISubdomainTestCase): def test_returns_empty_object(self): + """Return empty list when no names in database.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(url) @@ -31,6 +34,7 @@ class EmptyDatabaseTests(APISubdomainTestCase): self.assertEqual(response.json(), []) def test_returns_empty_list_with_get_all_param(self): + """Return empty list when no names and `random_items` param provided.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=5') @@ -38,6 +42,7 @@ class EmptyDatabaseTests(APISubdomainTestCase): self.assertEqual(response.json(), []) def test_returns_400_for_bad_random_items_param(self): + """Return error message when passing not integer as `random_items`.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=totally-a-valid-integer') @@ -47,6 +52,7 @@ class EmptyDatabaseTests(APISubdomainTestCase): }) def test_returns_400_for_negative_random_items_param(self): + """Return error message when passing negative int as `random_items`.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=-5') @@ -59,10 +65,11 @@ class EmptyDatabaseTests(APISubdomainTestCase): class ListTests(APISubdomainTestCase): @classmethod def setUpTestData(cls): - cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand') - cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk') + cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand', used=False) + cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk', used=True) def test_returns_name_in_list(self): + """Return all off-topic channel names.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(url) @@ -76,11 +83,21 @@ class ListTests(APISubdomainTestCase): ) def test_returns_single_item_with_random_items_param_set_to_1(self): + """Return not-used name instead used.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=1') self.assertEqual(response.status_code, 200) self.assertEqual(len(response.json()), 1) + self.assertEqual(response.json(), [self.test_name.name]) + + def test_running_out_of_names_with_random_parameter(self): + """Reset names `used` parameter to `False` when running out of names.""" + url = reverse('bot:offtopicchannelname-list', host='api') + response = self.client.get(f'{url}?random_items=2') + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), [self.test_name.name, self.test_name_2.name]) class CreationTests(APISubdomainTestCase): @@ -93,6 +110,7 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response.status_code, 201) def test_returns_201_for_unicode_chars(self): + """Accept all valid characters.""" url = reverse('bot:offtopicchannelname-list', host='api') names = ( '๐– ๐–ก๐–ข๐–ฃ๐–ค๐–ฅ๐–ฆ๐–ง๐–จ๐–ฉ๐–ช๐–ซ๐–ฌ๐–ญ๐–ฎ๐–ฏ๐–ฐ๐–ฑ๐–ฒ๐–ณ๐–ด๐–ต๐–ถ๐–ท๐–ธ๐–น', @@ -104,6 +122,7 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response.status_code, 201) def test_returns_400_for_missing_name_param(self): + """Return error message when name not provided.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.post(url) self.assertEqual(response.status_code, 400) @@ -112,6 +131,7 @@ class CreationTests(APISubdomainTestCase): }) def test_returns_400_for_bad_name_param(self): + """Return error message when invalid characters provided.""" url = reverse('bot:offtopicchannelname-list', host='api') invalid_names = ( 'space between words', @@ -134,18 +154,21 @@ class DeletionTests(APISubdomainTestCase): cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk') def test_deleting_unknown_name_returns_404(self): + """Return 404 reponse when trying to delete unknown name.""" url = reverse('bot:offtopicchannelname-detail', args=('unknown-name',), host='api') response = self.client.delete(url) self.assertEqual(response.status_code, 404) def test_deleting_known_name_returns_204(self): + """Return 204 response when deleting was successful.""" url = reverse('bot:offtopicchannelname-detail', args=(self.test_name.name,), host='api') response = self.client.delete(url) self.assertEqual(response.status_code, 204) def test_name_gets_deleted(self): + """Name gets actually deleted.""" url = reverse('bot:offtopicchannelname-detail', args=(self.test_name_2.name,), host='api') response = self.client.delete(url) diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py index d6da2399..826ad25e 100644 --- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py @@ -1,3 +1,4 @@ +from django.db.models import Case, Value, When from django.db.models.query import QuerySet from django.http.request import HttpRequest from django.shortcuts import get_object_or_404 @@ -20,7 +21,9 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): 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. + ... then the API will return `5` random items from the database + that is not used in current rotation. + When running out of names, API will mark all names to not used and start new rotation. #### Response format Return a list of off-topic-channel names: @@ -106,7 +109,27 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): 'random_items': ["Must be a positive integer."] }) - queryset = self.get_queryset().order_by('?')[:random_count] + queryset = self.get_queryset().order_by('used', '?')[:random_count] + + # When any name is used in our listing then this means we reached end of round + # and we need to reset all other names `used` to False + if any(offtopic_name.used for offtopic_name in queryset): + # These names that we just got have to be excluded from updating used to False + self.get_queryset().update( + used=Case( + When( + name__in=(offtopic_name.name for offtopic_name in queryset), + then=Value(True) + ), + default=Value(False) + ) + ) + else: + # Otherwise mark selected names `used` to True + self.get_queryset().filter( + name__in=(offtopic_name.name for offtopic_name in queryset) + ).update(used=True) + serialized = self.serializer_class(queryset, many=True) return Response(serialized.data) -- cgit v1.2.3 From 0ec3148228d7c13a35626059cffdad9dbb0670c2 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 30 Aug 2020 08:21:21 +0300 Subject: Revert "Fix mess with migrations" This reverts commit 32342aca3309b4fd482841cb84e4f166152d5c33. --- .../api/migrations/0051_create_news_setting.py | 25 ++++ .../api/migrations/0052_create_news_setting.py | 25 ---- .../migrations/0052_offtopicchannelname_used.py | 18 +++ .../api/migrations/0052_remove_user_avatar_hash.py | 17 +++ .../migrations/0053_offtopicchannelname_used.py | 18 --- .../api/migrations/0053_user_roles_to_array.py | 24 ++++ .../api/migrations/0054_remove_user_avatar_hash.py | 17 --- .../0054_user_invalidate_unknown_role.py | 21 +++ .../api/migrations/0055_merge_20200714_2027.py | 14 ++ .../apps/api/migrations/0055_reminder_mentions.py | 20 +++ .../api/migrations/0055_user_roles_to_array.py | 24 ---- .../api/migrations/0056_allow_blank_user_roles.py | 21 +++ .../0056_user_invalidate_unknown_role.py | 21 --- .../api/migrations/0057_merge_20200716_0751.py | 14 ++ .../apps/api/migrations/0057_reminder_mentions.py | 20 --- .../api/migrations/0058_allow_blank_user_roles.py | 21 --- .../migrations/0058_create_new_filterlist_model.py | 33 +++++ .../migrations/0059_create_new_filterlist_model.py | 33 ----- .../api/migrations/0059_populate_filterlists.py | 153 +++++++++++++++++++++ .../api/migrations/0060_populate_filterlists.py | 153 --------------------- .../migrations/0060_populate_filterlists_fix.py | 85 ++++++++++++ .../migrations/0061_populate_filterlists_fix.py | 85 ------------ 22 files changed, 445 insertions(+), 417 deletions(-) create mode 100644 pydis_site/apps/api/migrations/0051_create_news_setting.py delete mode 100644 pydis_site/apps/api/migrations/0052_create_news_setting.py create mode 100644 pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py create mode 100644 pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py delete mode 100644 pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py create mode 100644 pydis_site/apps/api/migrations/0053_user_roles_to_array.py delete mode 100644 pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py create mode 100644 pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py create mode 100644 pydis_site/apps/api/migrations/0055_merge_20200714_2027.py create mode 100644 pydis_site/apps/api/migrations/0055_reminder_mentions.py delete mode 100644 pydis_site/apps/api/migrations/0055_user_roles_to_array.py create mode 100644 pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py delete mode 100644 pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py create mode 100644 pydis_site/apps/api/migrations/0057_merge_20200716_0751.py delete mode 100644 pydis_site/apps/api/migrations/0057_reminder_mentions.py delete mode 100644 pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py create mode 100644 pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py delete mode 100644 pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py create mode 100644 pydis_site/apps/api/migrations/0059_populate_filterlists.py delete mode 100644 pydis_site/apps/api/migrations/0060_populate_filterlists.py create mode 100644 pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py delete mode 100644 pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py diff --git a/pydis_site/apps/api/migrations/0051_create_news_setting.py b/pydis_site/apps/api/migrations/0051_create_news_setting.py new file mode 100644 index 00000000..f18fdfb1 --- /dev/null +++ b/pydis_site/apps/api/migrations/0051_create_news_setting.py @@ -0,0 +1,25 @@ +from django.db import migrations + + +def up(apps, schema_editor): + BotSetting = apps.get_model('api', 'BotSetting') + setting = BotSetting( + name='news', + data={} + ).save() + + +def down(apps, schema_editor): + BotSetting = apps.get_model('api', 'BotSetting') + BotSetting.objects.get(name='news').delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0050_remove_infractions_active_default_value'), + ] + + operations = [ + migrations.RunPython(up, down) + ] diff --git a/pydis_site/apps/api/migrations/0052_create_news_setting.py b/pydis_site/apps/api/migrations/0052_create_news_setting.py deleted file mode 100644 index b101d19d..00000000 --- a/pydis_site/apps/api/migrations/0052_create_news_setting.py +++ /dev/null @@ -1,25 +0,0 @@ -from django.db import migrations - - -def up(apps, schema_editor): - BotSetting = apps.get_model('api', 'BotSetting') - setting = BotSetting( - name='news', - data={} - ).save() - - -def down(apps, schema_editor): - BotSetting = apps.get_model('api', 'BotSetting') - BotSetting.objects.get(name='news').delete() - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0051_allow_blank_message_embeds'), - ] - - operations = [ - migrations.RunPython(up, down) - ] diff --git a/pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py new file mode 100644 index 00000000..dfdf3835 --- /dev/null +++ b/pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.11 on 2020-03-30 10:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0051_create_news_setting'), + ] + + operations = [ + migrations.AddField( + model_name='offtopicchannelname', + name='used', + field=models.BooleanField(default=False, help_text='Whether or not this name has already been used during this rotation'), + ), + ] diff --git a/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py new file mode 100644 index 00000000..26b3b954 --- /dev/null +++ b/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.11 on 2020-05-27 07:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0051_create_news_setting'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='avatar_hash', + ), + ] diff --git a/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py deleted file mode 100644 index b51ce1d2..00000000 --- a/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.11 on 2020-03-30 10:24 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0052_create_news_setting'), - ] - - operations = [ - migrations.AddField( - model_name='offtopicchannelname', - name='used', - field=models.BooleanField(default=False, help_text='Whether or not this name has already been used during this rotation'), - ), - ] diff --git a/pydis_site/apps/api/migrations/0053_user_roles_to_array.py b/pydis_site/apps/api/migrations/0053_user_roles_to_array.py new file mode 100644 index 00000000..7ff3a548 --- /dev/null +++ b/pydis_site/apps/api/migrations/0053_user_roles_to_array.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.11 on 2020-06-02 13:42 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0052_remove_user_avatar_hash'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='roles', + ), + migrations.AddField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py deleted file mode 100644 index be9fd948..00000000 --- a/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.2.11 on 2020-05-27 07:17 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0053_offtopicchannelname_used'), - ] - - operations = [ - migrations.RemoveField( - model_name='user', - name='avatar_hash', - ), - ] diff --git a/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py new file mode 100644 index 00000000..96230015 --- /dev/null +++ b/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.11 on 2020-06-02 20:08 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.user + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0053_user_roles_to_array'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py b/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py new file mode 100644 index 00000000..f2a0e638 --- /dev/null +++ b/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.8 on 2020-07-14 20:27 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0051_allow_blank_message_embeds'), + ('api', '0054_user_invalidate_unknown_role'), + ] + + operations = [ + ] diff --git a/pydis_site/apps/api/migrations/0055_reminder_mentions.py b/pydis_site/apps/api/migrations/0055_reminder_mentions.py new file mode 100644 index 00000000..d73b450d --- /dev/null +++ b/pydis_site/apps/api/migrations/0055_reminder_mentions.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.14 on 2020-07-15 07:37 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0054_user_invalidate_unknown_role'), + ] + + operations = [ + migrations.AddField( + model_name='reminder', + name='mentions', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0055_user_roles_to_array.py b/pydis_site/apps/api/migrations/0055_user_roles_to_array.py deleted file mode 100644 index e7b4a983..00000000 --- a/pydis_site/apps/api/migrations/0055_user_roles_to_array.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.2.11 on 2020-06-02 13:42 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0054_remove_user_avatar_hash'), - ] - - operations = [ - migrations.RemoveField( - model_name='user', - name='roles', - ), - migrations.AddField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py new file mode 100644 index 00000000..489941c7 --- /dev/null +++ b/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0.8 on 2020-07-14 20:35 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.user + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0055_merge_20200714_2027'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py deleted file mode 100644 index ab2696aa..00000000 --- a/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 2.2.11 on 2020-06-02 20:08 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models -import pydis_site.apps.api.models.bot.user - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0055_user_roles_to_array'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py b/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py new file mode 100644 index 00000000..47a6d2d4 --- /dev/null +++ b/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.14 on 2020-07-16 07:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0055_reminder_mentions'), + ('api', '0056_allow_blank_user_roles'), + ] + + operations = [ + ] diff --git a/pydis_site/apps/api/migrations/0057_reminder_mentions.py b/pydis_site/apps/api/migrations/0057_reminder_mentions.py deleted file mode 100644 index fb829a17..00000000 --- a/pydis_site/apps/api/migrations/0057_reminder_mentions.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.2.14 on 2020-07-15 07:37 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0056_user_invalidate_unknown_role'), - ] - - operations = [ - migrations.AddField( - model_name='reminder', - name='mentions', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py deleted file mode 100644 index 8f7fddfc..00000000 --- a/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-14 20:35 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models -import pydis_site.apps.api.models.bot.user - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0057_reminder_mentions'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py new file mode 100644 index 00000000..aecfdad7 --- /dev/null +++ b/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py @@ -0,0 +1,33 @@ +# Generated by Django 3.0.8 on 2020-07-15 11:23 + +from django.db import migrations, models +import pydis_site.apps.api.models.mixins + + +class Migration(migrations.Migration): + dependencies = [ + ('api', '0057_merge_20200716_0751'), + ] + + operations = [ + migrations.CreateModel( + name='FilterList', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('type', models.CharField( + choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), + ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], + help_text='The type of allowlist this is on.', max_length=50)), + ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), + ('content', models.TextField(help_text='The data to add to the allow or denylist.')), + ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), + ], + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), + ), + migrations.AddConstraint( + model_name='filterlist', + constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list') + ) + ] diff --git a/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py deleted file mode 100644 index eac5542d..00000000 --- a/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-15 11:23 - -from django.db import migrations, models -import pydis_site.apps.api.models.mixins - - -class Migration(migrations.Migration): - dependencies = [ - ('api', '0058_allow_blank_user_roles'), - ] - - operations = [ - migrations.CreateModel( - name='FilterList', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('type', models.CharField( - choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), - ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], - help_text='The type of allowlist this is on.', max_length=50)), - ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), - ('content', models.TextField(help_text='The data to add to the allow or denylist.')), - ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), - ], - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - migrations.AddConstraint( - model_name='filterlist', - constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list') - ) - ] diff --git a/pydis_site/apps/api/migrations/0059_populate_filterlists.py b/pydis_site/apps/api/migrations/0059_populate_filterlists.py new file mode 100644 index 00000000..8c550191 --- /dev/null +++ b/pydis_site/apps/api/migrations/0059_populate_filterlists.py @@ -0,0 +1,153 @@ +from django.db import migrations + +guild_invite_whitelist = [ + ("discord.gg/python", "Python Discord", True), + ("discord.gg/4JJdJKb", "RLBot", True), + ("discord.gg/djPtTRJ", "Kivy", True), + ("discord.gg/QXyegWe", "Pyglet", True), + ("discord.gg/9XsucTT", "Panda3D", True), + ("discord.gg/AP3rq2k", "PyWeek", True), + ("discord.gg/vSPsP9t", "Microsoft Python", True), + ("discord.gg/bRCvFy9", "Discord.js Official", True), + ("discord.gg/9zT7NHP", "Programming Discussions", True), + ("discord.gg/ysd6M4r", "JetBrains Community", True), + ("discord.gg/4xJeCgy", "Raspberry Pie", True), + ("discord.gg/AStb3kZ", "Ren'Py", True), + ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), + ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), + ("discord.gg/jTtgWuy", "Django", True), + ("discord.gg/W9BypZF", "STEM", True), + ("discord.gg/dpy", "discord.py", True), + ("discord.gg/programming", "Programmers Hangout", True), + ("discord.gg/qhGUjGD", "SpeakJS", True), + ("discord.gg/eTbWSZj", "Functional Programming", True), + ("discord.gg/r8yreB6", "PyGame", True), + ("discord.gg/5UBnR3P", "Python Atlanta", True), + ("discord.gg/ccyrDKv", "C#", True), +] + +domain_name_blacklist = [ + ("pornhub.com", None, False), + ("liveleak.com", None, False), + ("grabify.link", None, False), + ("bmwforum.co", None, False), + ("leancoding.co", None, False), + ("spottyfly.com", None, False), + ("stopify.co", None, False), + ("yoรผtu.be", None, False), + ("discรถrd.com", None, False), + ("minecrรคft.com", None, False), + ("freegiftcards.co", None, False), + ("disรงordapp.com", None, False), + ("fortnight.space", None, False), + ("fortnitechat.site", None, False), + ("joinmy.site", None, False), + ("curiouscat.club", None, False), + ("catsnthings.fun", None, False), + ("yourtube.site", None, False), + ("youtubeshort.watch", None, False), + ("catsnthing.com", None, False), + ("youtubeshort.pro", None, False), + ("canadianlumberjacks.online", None, False), + ("poweredbydialup.club", None, False), + ("poweredbydialup.online", None, False), + ("poweredbysecurity.org", None, False), + ("poweredbysecurity.online", None, False), + ("ssteam.site", None, False), + ("steamwalletgift.com", None, False), + ("discord.gift", None, False), + ("lmgtfy.com", None, False), +] + +filter_token_blacklist = [ + ("\bgoo+ks*\b", None, False), + ("\bky+s+\b", None, False), + ("\bki+ke+s*\b", None, False), + ("\bbeaner+s?\b", None, False), + ("\bcoo+ns*\b", None, False), + ("\bnig+lets*\b", None, False), + ("\bslant-eyes*\b", None, False), + ("\btowe?l-?head+s*\b", None, False), + ("\bchi*n+k+s*\b", None, False), + ("\bspick*s*\b", None, False), + ("\bkill* +(?:yo)?urself+\b", None, False), + ("\bjew+s*\b", None, False), + ("\bsuicide\b", None, False), + ("\brape\b", None, False), + ("\b(re+)tar+(d+|t+)(ed)?\b", None, False), + ("\bta+r+d+\b", None, False), + ("\bcunts*\b", None, False), + ("\btrann*y\b", None, False), + ("\bshemale\b", None, False), + ("fa+g+s*", None, False), + ("ๅ", None, False), + ("ๅ", None, False), + ("เฟ–", None, False), + ("เฟ•", None, False), + ("เฟ˜", None, False), + ("เฟ—", None, False), + ("cuck(?!oo+)", None, False), + ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), + ("fag+o+t+s*", None, False), +] + +file_format_whitelist = [ + (".3gp", None, True), + (".3g2", None, True), + (".avi", None, True), + (".bmp", None, True), + (".gif", None, True), + (".h264", None, True), + (".jpg", None, True), + (".jpeg", None, True), + (".m4v", None, True), + (".mkv", None, True), + (".mov", None, True), + (".mp4", None, True), + (".mpeg", None, True), + (".mpg", None, True), + (".png", None, True), + (".tiff", None, True), + (".wmv", None, True), + (".svg", None, True), + (".psd", "Photoshop", True), + (".ai", "Illustrator", True), + (".aep", "After Effects", True), + (".xcf", "GIMP", True), + (".mp3", None, True), + (".wav", None, True), + (".ogg", None, True), + (".webm", None, True), + (".webp", None, True), +] + +populate_data = { + "FILTER_TOKEN": filter_token_blacklist, + "DOMAIN_NAME": domain_name_blacklist, + "FILE_FORMAT": file_format_whitelist, + "GUILD_INVITE": guild_invite_whitelist, +} + + +class Migration(migrations.Migration): + dependencies = [("api", "0058_create_new_filterlist_model")] + + def populate_filterlists(app, _): + FilterList = app.get_model("api", "FilterList") + + for filterlist_type, metadata in populate_data.items(): + for content, comment, allowed in metadata: + FilterList.objects.create( + type=filterlist_type, + allowed=allowed, + content=content, + comment=comment, + ) + + def clear_filterlists(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.all().delete() + + operations = [ + migrations.RunPython(populate_filterlists, clear_filterlists) + ] diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists.py b/pydis_site/apps/api/migrations/0060_populate_filterlists.py deleted file mode 100644 index 35fde95a..00000000 --- a/pydis_site/apps/api/migrations/0060_populate_filterlists.py +++ /dev/null @@ -1,153 +0,0 @@ -from django.db import migrations - -guild_invite_whitelist = [ - ("discord.gg/python", "Python Discord", True), - ("discord.gg/4JJdJKb", "RLBot", True), - ("discord.gg/djPtTRJ", "Kivy", True), - ("discord.gg/QXyegWe", "Pyglet", True), - ("discord.gg/9XsucTT", "Panda3D", True), - ("discord.gg/AP3rq2k", "PyWeek", True), - ("discord.gg/vSPsP9t", "Microsoft Python", True), - ("discord.gg/bRCvFy9", "Discord.js Official", True), - ("discord.gg/9zT7NHP", "Programming Discussions", True), - ("discord.gg/ysd6M4r", "JetBrains Community", True), - ("discord.gg/4xJeCgy", "Raspberry Pie", True), - ("discord.gg/AStb3kZ", "Ren'Py", True), - ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), - ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), - ("discord.gg/jTtgWuy", "Django", True), - ("discord.gg/W9BypZF", "STEM", True), - ("discord.gg/dpy", "discord.py", True), - ("discord.gg/programming", "Programmers Hangout", True), - ("discord.gg/qhGUjGD", "SpeakJS", True), - ("discord.gg/eTbWSZj", "Functional Programming", True), - ("discord.gg/r8yreB6", "PyGame", True), - ("discord.gg/5UBnR3P", "Python Atlanta", True), - ("discord.gg/ccyrDKv", "C#", True), -] - -domain_name_blacklist = [ - ("pornhub.com", None, False), - ("liveleak.com", None, False), - ("grabify.link", None, False), - ("bmwforum.co", None, False), - ("leancoding.co", None, False), - ("spottyfly.com", None, False), - ("stopify.co", None, False), - ("yoรผtu.be", None, False), - ("discรถrd.com", None, False), - ("minecrรคft.com", None, False), - ("freegiftcards.co", None, False), - ("disรงordapp.com", None, False), - ("fortnight.space", None, False), - ("fortnitechat.site", None, False), - ("joinmy.site", None, False), - ("curiouscat.club", None, False), - ("catsnthings.fun", None, False), - ("yourtube.site", None, False), - ("youtubeshort.watch", None, False), - ("catsnthing.com", None, False), - ("youtubeshort.pro", None, False), - ("canadianlumberjacks.online", None, False), - ("poweredbydialup.club", None, False), - ("poweredbydialup.online", None, False), - ("poweredbysecurity.org", None, False), - ("poweredbysecurity.online", None, False), - ("ssteam.site", None, False), - ("steamwalletgift.com", None, False), - ("discord.gift", None, False), - ("lmgtfy.com", None, False), -] - -filter_token_blacklist = [ - ("\bgoo+ks*\b", None, False), - ("\bky+s+\b", None, False), - ("\bki+ke+s*\b", None, False), - ("\bbeaner+s?\b", None, False), - ("\bcoo+ns*\b", None, False), - ("\bnig+lets*\b", None, False), - ("\bslant-eyes*\b", None, False), - ("\btowe?l-?head+s*\b", None, False), - ("\bchi*n+k+s*\b", None, False), - ("\bspick*s*\b", None, False), - ("\bkill* +(?:yo)?urself+\b", None, False), - ("\bjew+s*\b", None, False), - ("\bsuicide\b", None, False), - ("\brape\b", None, False), - ("\b(re+)tar+(d+|t+)(ed)?\b", None, False), - ("\bta+r+d+\b", None, False), - ("\bcunts*\b", None, False), - ("\btrann*y\b", None, False), - ("\bshemale\b", None, False), - ("fa+g+s*", None, False), - ("ๅ", None, False), - ("ๅ", None, False), - ("เฟ–", None, False), - ("เฟ•", None, False), - ("เฟ˜", None, False), - ("เฟ—", None, False), - ("cuck(?!oo+)", None, False), - ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), - ("fag+o+t+s*", None, False), -] - -file_format_whitelist = [ - (".3gp", None, True), - (".3g2", None, True), - (".avi", None, True), - (".bmp", None, True), - (".gif", None, True), - (".h264", None, True), - (".jpg", None, True), - (".jpeg", None, True), - (".m4v", None, True), - (".mkv", None, True), - (".mov", None, True), - (".mp4", None, True), - (".mpeg", None, True), - (".mpg", None, True), - (".png", None, True), - (".tiff", None, True), - (".wmv", None, True), - (".svg", None, True), - (".psd", "Photoshop", True), - (".ai", "Illustrator", True), - (".aep", "After Effects", True), - (".xcf", "GIMP", True), - (".mp3", None, True), - (".wav", None, True), - (".ogg", None, True), - (".webm", None, True), - (".webp", None, True), -] - -populate_data = { - "FILTER_TOKEN": filter_token_blacklist, - "DOMAIN_NAME": domain_name_blacklist, - "FILE_FORMAT": file_format_whitelist, - "GUILD_INVITE": guild_invite_whitelist, -} - - -class Migration(migrations.Migration): - dependencies = [("api", "0059_create_new_filterlist_model")] - - def populate_filterlists(app, _): - FilterList = app.get_model("api", "FilterList") - - for filterlist_type, metadata in populate_data.items(): - for content, comment, allowed in metadata: - FilterList.objects.create( - type=filterlist_type, - allowed=allowed, - content=content, - comment=comment, - ) - - def clear_filterlists(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.all().delete() - - operations = [ - migrations.RunPython(populate_filterlists, clear_filterlists) - ] diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py new file mode 100644 index 00000000..53846f02 --- /dev/null +++ b/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py @@ -0,0 +1,85 @@ +from django.db import migrations + +bad_guild_invite_whitelist = [ + ("discord.gg/python", "Python Discord", True), + ("discord.gg/4JJdJKb", "RLBot", True), + ("discord.gg/djPtTRJ", "Kivy", True), + ("discord.gg/QXyegWe", "Pyglet", True), + ("discord.gg/9XsucTT", "Panda3D", True), + ("discord.gg/AP3rq2k", "PyWeek", True), + ("discord.gg/vSPsP9t", "Microsoft Python", True), + ("discord.gg/bRCvFy9", "Discord.js Official", True), + ("discord.gg/9zT7NHP", "Programming Discussions", True), + ("discord.gg/ysd6M4r", "JetBrains Community", True), + ("discord.gg/4xJeCgy", "Raspberry Pie", True), + ("discord.gg/AStb3kZ", "Ren'Py", True), + ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), + ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), + ("discord.gg/jTtgWuy", "Django", True), + ("discord.gg/W9BypZF", "STEM", True), + ("discord.gg/dpy", "discord.py", True), + ("discord.gg/programming", "Programmers Hangout", True), + ("discord.gg/qhGUjGD", "SpeakJS", True), + ("discord.gg/eTbWSZj", "Functional Programming", True), + ("discord.gg/r8yreB6", "PyGame", True), + ("discord.gg/5UBnR3P", "Python Atlanta", True), + ("discord.gg/ccyrDKv", "C#", True), +] + +guild_invite_whitelist = [ + ("267624335836053506", "Python Discord", True), + ("348658686962696195", "RLBot", True), + ("423249981340778496", "Kivy", True), + ("438622377094414346", "Pyglet", True), + ("524691714909274162", "Panda3D", True), + ("666560367173828639", "PyWeek", True), + ("702724176489873509", "Microsoft Python", True), + ("222078108977594368", "Discord.js Official", True), + ("238666723824238602", "Programming Discussions", True), + ("433980600391696384", "JetBrains Community", True), + ("204621105720328193", "Raspberry Pie", True), + ("286633898581164032", "Ren'Py", True), + ("440186186024222721", "Python Discord: Emojis 1", True), + ("578587418123304970", "Python Discord: Emojis 2", True), + ("159039020565790721", "Django", True), + ("273944235143593984", "STEM", True), + ("336642139381301249", "discord.py", True), + ("244230771232079873", "Programmers Hangout", True), + ("239433591950540801", "SpeakJS", True), + ("280033776820813825", "Functional Programming", True), + ("349505959032389632", "PyGame", True), + ("488751051629920277", "Python Atlanta", True), + ("143867839282020352", "C#", True), +] + + +class Migration(migrations.Migration): + dependencies = [("api", "0059_populate_filterlists")] + + def fix_filterlist(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.filter(type="GUILD_INVITE").delete() # Clear out the stuff added in 0059. + + for content, comment, allowed in guild_invite_whitelist: + FilterList.objects.create( + type="GUILD_INVITE", + allowed=allowed, + content=content, + comment=comment, + ) + + def restore_bad_filterlist(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.filter(type="GUILD_INVITE").delete() + + for content, comment, allowed in bad_guild_invite_whitelist: + FilterList.objects.create( + type="GUILD_INVITE", + allowed=allowed, + content=content, + comment=comment, + ) + + operations = [ + migrations.RunPython(fix_filterlist, restore_bad_filterlist) + ] diff --git a/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py deleted file mode 100644 index eaaafb38..00000000 --- a/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py +++ /dev/null @@ -1,85 +0,0 @@ -from django.db import migrations - -bad_guild_invite_whitelist = [ - ("discord.gg/python", "Python Discord", True), - ("discord.gg/4JJdJKb", "RLBot", True), - ("discord.gg/djPtTRJ", "Kivy", True), - ("discord.gg/QXyegWe", "Pyglet", True), - ("discord.gg/9XsucTT", "Panda3D", True), - ("discord.gg/AP3rq2k", "PyWeek", True), - ("discord.gg/vSPsP9t", "Microsoft Python", True), - ("discord.gg/bRCvFy9", "Discord.js Official", True), - ("discord.gg/9zT7NHP", "Programming Discussions", True), - ("discord.gg/ysd6M4r", "JetBrains Community", True), - ("discord.gg/4xJeCgy", "Raspberry Pie", True), - ("discord.gg/AStb3kZ", "Ren'Py", True), - ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), - ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), - ("discord.gg/jTtgWuy", "Django", True), - ("discord.gg/W9BypZF", "STEM", True), - ("discord.gg/dpy", "discord.py", True), - ("discord.gg/programming", "Programmers Hangout", True), - ("discord.gg/qhGUjGD", "SpeakJS", True), - ("discord.gg/eTbWSZj", "Functional Programming", True), - ("discord.gg/r8yreB6", "PyGame", True), - ("discord.gg/5UBnR3P", "Python Atlanta", True), - ("discord.gg/ccyrDKv", "C#", True), -] - -guild_invite_whitelist = [ - ("267624335836053506", "Python Discord", True), - ("348658686962696195", "RLBot", True), - ("423249981340778496", "Kivy", True), - ("438622377094414346", "Pyglet", True), - ("524691714909274162", "Panda3D", True), - ("666560367173828639", "PyWeek", True), - ("702724176489873509", "Microsoft Python", True), - ("222078108977594368", "Discord.js Official", True), - ("238666723824238602", "Programming Discussions", True), - ("433980600391696384", "JetBrains Community", True), - ("204621105720328193", "Raspberry Pie", True), - ("286633898581164032", "Ren'Py", True), - ("440186186024222721", "Python Discord: Emojis 1", True), - ("578587418123304970", "Python Discord: Emojis 2", True), - ("159039020565790721", "Django", True), - ("273944235143593984", "STEM", True), - ("336642139381301249", "discord.py", True), - ("244230771232079873", "Programmers Hangout", True), - ("239433591950540801", "SpeakJS", True), - ("280033776820813825", "Functional Programming", True), - ("349505959032389632", "PyGame", True), - ("488751051629920277", "Python Atlanta", True), - ("143867839282020352", "C#", True), -] - - -class Migration(migrations.Migration): - dependencies = [("api", "0060_populate_filterlists")] - - def fix_filterlist(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.filter(type="GUILD_INVITE").delete() # Clear out the stuff added in 0059. - - for content, comment, allowed in guild_invite_whitelist: - FilterList.objects.create( - type="GUILD_INVITE", - allowed=allowed, - content=content, - comment=comment, - ) - - def restore_bad_filterlist(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.filter(type="GUILD_INVITE").delete() - - for content, comment, allowed in bad_guild_invite_whitelist: - FilterList.objects.create( - type="GUILD_INVITE", - allowed=allowed, - content=content, - comment=comment, - ) - - operations = [ - migrations.RunPython(fix_filterlist, restore_bad_filterlist) - ] -- cgit v1.2.3 From ab634c6ef1d4b7cec3b8471262eec3ec727478d8 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 30 Aug 2020 08:27:07 +0300 Subject: Create merge migration to fix conflicts --- pydis_site/apps/api/migrations/0061_merge_20200830_0526.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0061_merge_20200830_0526.py diff --git a/pydis_site/apps/api/migrations/0061_merge_20200830_0526.py b/pydis_site/apps/api/migrations/0061_merge_20200830_0526.py new file mode 100644 index 00000000..f0668696 --- /dev/null +++ b/pydis_site/apps/api/migrations/0061_merge_20200830_0526.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.8 on 2020-08-30 05:26 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0060_populate_filterlists_fix'), + ('api', '0052_offtopicchannelname_used'), + ] + + operations = [ + ] -- cgit v1.2.3 From b184e3bd943ad05b1d75449fa4bc564c62a31726 Mon Sep 17 00:00:00 2001 From: Derek Date: Sun, 30 Aug 2020 16:42:31 -0400 Subject: Update README.md update server's user count --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ec2f0af3..616f2edc 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Python Discord: Site -[![Discord](https://img.shields.io/static/v1?label=Python%20Discord&logo=discord&message=%3E30k%20members&color=%237289DA&logoColor=white)](https://discord.gg/2B963hn) +[![Discord](https://img.shields.io/static/v1?label=Python%20Discord&logo=discord&message=%3E95k%20members&color=%237289DA&logoColor=white)](https://discord.gg/2B963hn) [![Build Status](https://dev.azure.com/python-discord/Python%20Discord/_apis/build/status/Site?branchName=master)](https://dev.azure.com/python-discord/Python%20Discord/_build/latest?definitionId=2&branchName=master) [![Tests](https://img.shields.io/azure-devops/tests/python-discord/Python%20Discord/2?compact_message)](https://dev.azure.com/python-discord/Python%20Discord/_apis/build/status/Site?branchName=master) [![Coverage](https://img.shields.io/azure-devops/coverage/python-discord/Python%20Discord/2/master)](https://dev.azure.com/python-discord/Python%20Discord/_apis/build/status/Site?branchName=master) -- cgit v1.2.3 From a9e1f6f398ca4cac42c01a211513c02881e7541d Mon Sep 17 00:00:00 2001 From: Karlis S Date: Tue, 1 Sep 2020 14:59:23 +0000 Subject: Create new merge migration to fix conflicts between migrations --- pydis_site/apps/api/migrations/0062_merge_20200901_1459.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0062_merge_20200901_1459.py diff --git a/pydis_site/apps/api/migrations/0062_merge_20200901_1459.py b/pydis_site/apps/api/migrations/0062_merge_20200901_1459.py new file mode 100644 index 00000000..d162acf1 --- /dev/null +++ b/pydis_site/apps/api/migrations/0062_merge_20200901_1459.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.8 on 2020-09-01 14:59 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0051_delete_tag'), + ('api', '0061_merge_20200830_0526'), + ] + + operations = [ + ] -- cgit v1.2.3 From 01a0e825333812dff21351201fce8c25031cc8fa Mon Sep 17 00:00:00 2001 From: Leon Sandรธy Date: Thu, 3 Sep 2020 00:39:25 +0200 Subject: Update landing page. Replace flake8-annotations with metricity. --- pydis_site/apps/home/tests/mock_github_api_response.json | 2 +- pydis_site/apps/home/views/home.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pydis_site/apps/home/tests/mock_github_api_response.json b/pydis_site/apps/home/tests/mock_github_api_response.json index 35604a85..10be4f99 100644 --- a/pydis_site/apps/home/tests/mock_github_api_response.json +++ b/pydis_site/apps/home/tests/mock_github_api_response.json @@ -28,7 +28,7 @@ "forks_count": 31 }, { - "full_name": "python-discord/flake8-annotations", + "full_name": "python-discord/metricity", "description": "test", "stargazers_count": 97, "language": "Python", diff --git a/pydis_site/apps/home/views/home.py b/pydis_site/apps/home/views/home.py index 20e38ab0..3b5cd5ac 100644 --- a/pydis_site/apps/home/views/home.py +++ b/pydis_site/apps/home/views/home.py @@ -23,7 +23,7 @@ class HomeView(View): "python-discord/bot", "python-discord/snekbox", "python-discord/seasonalbot", - "python-discord/flake8-annotations", + "python-discord/metricity", "python-discord/django-simple-bulma", ] -- cgit v1.2.3 From 1b07a0021ad61f21f4414e8bb9045d605ce87f64 Mon Sep 17 00:00:00 2001 From: Derek Date: Wed, 2 Sep 2020 22:17:44 -0400 Subject: add timeline view add timeline route --- pydis_site/apps/home/urls.py | 3 ++- pydis_site/apps/home/views/__init__.py | 4 ++-- pydis_site/apps/home/views/home.py | 5 +++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pydis_site/apps/home/urls.py b/pydis_site/apps/home/urls.py index 61e87a39..cf0c7457 100644 --- a/pydis_site/apps/home/urls.py +++ b/pydis_site/apps/home/urls.py @@ -6,7 +6,7 @@ from django.contrib.messages import ERROR from django.urls import include, path from pydis_site.utils.views import MessageRedirectView -from .views import AccountDeleteView, AccountSettingsView, HomeView +from .views import AccountDeleteView, AccountSettingsView, HomeView, timeline app_name = 'home' urlpatterns = [ @@ -38,4 +38,5 @@ urlpatterns = [ path('admin/', admin.site.urls), path('notifications/', include('django_nyt.urls')), + path('timeline/', timeline), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/pydis_site/apps/home/views/__init__.py b/pydis_site/apps/home/views/__init__.py index 801fd398..36b88b1b 100644 --- a/pydis_site/apps/home/views/__init__.py +++ b/pydis_site/apps/home/views/__init__.py @@ -1,4 +1,4 @@ from .account import DeleteView as AccountDeleteView, SettingsView as AccountSettingsView -from .home import HomeView +from .home import HomeView, timeline -__all__ = ["AccountDeleteView", "AccountSettingsView", "HomeView"] +__all__ = ["AccountDeleteView", "AccountSettingsView", "HomeView", "timeline"] diff --git a/pydis_site/apps/home/views/home.py b/pydis_site/apps/home/views/home.py index 20e38ab0..7ad995cc 100644 --- a/pydis_site/apps/home/views/home.py +++ b/pydis_site/apps/home/views/home.py @@ -126,3 +126,8 @@ class HomeView(View): """Collect repo data and render the homepage view.""" repo_data = self._get_repo_data() return render(request, "home/index.html", {"repo_data": repo_data}) + + +def timeline(request: WSGIRequest) -> HttpResponse: + """Render timeline view.""" + return render(request, 'home/timeline.html') -- cgit v1.2.3 From 6b692e992666f78c4b03e34b19c1ad8f8ca6ab8c Mon Sep 17 00:00:00 2001 From: Derek Date: Wed, 2 Sep 2020 22:20:17 -0400 Subject: add timeline template rough first draft --- pydis_site/templates/home/timeline.html | 475 ++++++++++++++++++++++++++++++++ 1 file changed, 475 insertions(+) create mode 100644 pydis_site/templates/home/timeline.html diff --git a/pydis_site/templates/home/timeline.html b/pydis_site/templates/home/timeline.html new file mode 100644 index 00000000..d9cdd625 --- /dev/null +++ b/pydis_site/templates/home/timeline.html @@ -0,0 +1,475 @@ +{% extends 'base/base.html' %} +{% load static %} + +{% block title %}Home{% endblock %} +{% block head %} + + +{% endblock %} + +{% block content %} + {% include "base/navbar.html" %} + +
+
+ + +
+ +
+ Picture +
+ +
+

Python Discord hits 1,000 members

+

lorem ipsum

+ +
+ Nov 10th, 2017 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

Our logo is born. Thanks Aperture!

+

picture here?

+ +
+ Feb 3rd, 2018 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

PyDis hits 2,000 members; pythondiscord.com and @Python are live

+

lorem ipsum

+ +
+ Mar 4th, 2018 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

First code jam with the theme โ€œsnakesโ€

+

lorem ipsum

+ +
+ Mar 23rd, 2018 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

The privacy policy is created

+

lorem ipsum

+ +
+ May 21st, 2018 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

Do You Even Python and PyDis merger

+

lorem ipsum

+ +
+ Jun 9th, 2018 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

PyDis hits 5,000 members

+

lorem ipsum

+ +
+ Jun 20th, 2018 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

PyDis is now partnered with Discord; the vanity URL discord.gg/python is created

+

lorem ipsum

+ +
+ Jul 10th, 2018 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

First Hacktoberfest PyDis event; @SeasonalBot is created

+

lorem ipsum

+ +
+ Oct 1st, 2018 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

PyDis hits 10,000 members

+

lorem ipsum

+ +
+ Nov 24th, 2018 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

django-simple-bulma is released on PyPi

+

lorem ipsum

+ +
+ Dec 19th, 2018 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

PyDis hits 15,000 members; the โ€œhot ones specialโ€ video is released

+

lorem ipsum

+ +
+ Apr 8th, 2019 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

the Django rewrite of pythondiscord.com is now live!

+

lorem ipsum

+ +
+ Sep 15, 2019 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

flake8-annotations is released on PyPI

+

lorem ipsum

+ +
+ Sep 25th, 2019 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

The code of conduct is created

+

lorem ipsum

+ +
+ Oct 26th, 2019 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

PyDis hits 30,000 members

+

lorem ipsum

+ +
+ Dec 22nd, 2019 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

PyDis sixth code jam with the theme โ€œAncient technologyโ€ and the technology Kivy

+

lorem ipsum

+ +
+ Jan 17, 2020 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

The new help channel system is live

+

lorem ipsum

+ +
+ Apr 5th, 2020 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

Python Discord hits 40,000 members, and is now bigger than Liechtenstein.

+

picture here?

+ +
+ Apr 14, 2020 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

PyDis Game Jam 2020 with the โ€œThree of a Kindโ€ theme and Arcade as the technology

+

lorem ipsum

+ +
+ Apr 17th, 2020 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

ModMail is now live

+

lorem ipsum

+ +
+ May 25th, 2020 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

Python Discord is now listed on python.org/community

+

lorem ipsum

+ +
+ May 28th, 2020 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

Python Discord Public Statistics are now live

+

lorem ipsum

+ +
+ Jun 4th, 2020 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

PyDis summer code jam 2020 with the theme โ€œEarly Internetโ€ and Django as the technology

+

lorem ipsum

+ +
+ Jul 31st, 2020 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

Python Discord is now the new home of the PyWeek event!

+

lorem ipsum

+ +
+ Aug 16th, 2020 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

Python Discord hits 100,000 members.

+

lorem ipsum

+ +
+ Sep ??, 2020 + Read more +
+
+
+
+
+ + {% endblock %} -- cgit v1.2.3 From e1382867ab2a96cf1b656b9b9f5154c0e7fa6aac Mon Sep 17 00:00:00 2001 From: Derek Date: Wed, 2 Sep 2020 22:28:19 -0400 Subject: add static timeline assets --- pydis_site/static/css/home/style.css | 1 + .../static/images/timeline/cd-icon-location.svg | 4 ++ .../static/images/timeline/cd-icon-movie.svg | 4 ++ .../static/images/timeline/cd-icon-picture.svg | 5 ++ pydis_site/static/js/timeline/main.js | 71 ++++++++++++++++++++++ 5 files changed, 85 insertions(+) create mode 100644 pydis_site/static/css/home/style.css create mode 100755 pydis_site/static/images/timeline/cd-icon-location.svg create mode 100755 pydis_site/static/images/timeline/cd-icon-movie.svg create mode 100755 pydis_site/static/images/timeline/cd-icon-picture.svg create mode 100644 pydis_site/static/js/timeline/main.js diff --git a/pydis_site/static/css/home/style.css b/pydis_site/static/css/home/style.css new file mode 100644 index 00000000..4eaca7c2 --- /dev/null +++ b/pydis_site/static/css/home/style.css @@ -0,0 +1 @@ +@import url("https://fonts.googleapis.com/css?family=Droid+Serif|Open+Sans:400,700");*,*::after,*::before{box-sizing:inherit}*{font:inherit}html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video,hr{margin:0;padding:0;border:0}html{box-sizing:border-box}body{background-color:hsl(0, 0%, 100%);background-color:var(--color-bg, white)}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section,main,form legend{display:block}ol,ul{list-style:none}blockquote,q{quotes:none}button,input,textarea,select{margin:0}.btn,.form-control,.link,.reset{background-color:transparent;padding:0;border:0;border-radius:0;color:inherit;line-height:inherit;-webkit-appearance:none;-moz-appearance:none;appearance:none}select.form-control::-ms-expand{display:none}textarea{resize:vertical;overflow:auto;vertical-align:top}input::-ms-clear{display:none}table{border-collapse:collapse;border-spacing:0}img,video,svg{max-width:100%}[data-theme]{background-color:hsl(0, 0%, 100%);background-color:var(--color-bg, #fff);color:hsl(240, 4%, 20%);color:var(--color-contrast-high, #313135)}:root{--space-unit: 1em;--space-xxxxs: calc(0.125*var(--space-unit));--space-xxxs: calc(0.25*var(--space-unit));--space-xxs: calc(0.375*var(--space-unit));--space-xs: calc(0.5*var(--space-unit));--space-sm: calc(0.75*var(--space-unit));--space-md: calc(1.25*var(--space-unit));--space-lg: calc(2*var(--space-unit));--space-xl: calc(3.25*var(--space-unit));--space-xxl: calc(5.25*var(--space-unit));--space-xxxl: calc(8.5*var(--space-unit));--space-xxxxl: calc(13.75*var(--space-unit));--component-padding: var(--space-md)}:root{--max-width-xxs: 32rem;--max-width-xs: 38rem;--max-width-sm: 48rem;--max-width-md: 64rem;--max-width-lg: 80rem;--max-width-xl: 90rem;--max-width-xxl: 120rem}.container{width:calc(100% - 1.25em);width:calc(100% - 2*var(--component-padding));margin-left:auto;margin-right:auto}.max-width-xxs{max-width:32rem;max-width:var(--max-width-xxs)}.max-width-xs{max-width:38rem;max-width:var(--max-width-xs)}.max-width-sm{max-width:48rem;max-width:var(--max-width-sm)}.max-width-md{max-width:64rem;max-width:var(--max-width-md)}.max-width-lg{max-width:80rem;max-width:var(--max-width-lg)}.max-width-xl{max-width:90rem;max-width:var(--max-width-xl)}.max-width-xxl{max-width:120rem;max-width:var(--max-width-xxl)}.max-width-adaptive-sm{max-width:38rem;max-width:var(--max-width-xs)}@media (min-width: 64rem){.max-width-adaptive-sm{max-width:48rem;max-width:var(--max-width-sm)}}.max-width-adaptive-md{max-width:38rem;max-width:var(--max-width-xs)}@media (min-width: 64rem){.max-width-adaptive-md{max-width:64rem;max-width:var(--max-width-md)}}.max-width-adaptive,.max-width-adaptive-lg{max-width:38rem;max-width:var(--max-width-xs)}@media (min-width: 64rem){.max-width-adaptive,.max-width-adaptive-lg{max-width:64rem;max-width:var(--max-width-md)}}@media (min-width: 90rem){.max-width-adaptive,.max-width-adaptive-lg{max-width:80rem;max-width:var(--max-width-lg)}}.max-width-adaptive-xl{max-width:38rem;max-width:var(--max-width-xs)}@media (min-width: 64rem){.max-width-adaptive-xl{max-width:64rem;max-width:var(--max-width-md)}}@media (min-width: 90rem){.max-width-adaptive-xl{max-width:90rem;max-width:var(--max-width-xl)}}.grid{--grid-gap: 0px;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.grid>*{-ms-flex-preferred-size:100%;flex-basis:100%}[class*="grid-gap"]{margin-bottom:1em * -1;margin-bottom:calc(var(--grid-gap, 1em)*-1);margin-right:1em * -1;margin-right:calc(var(--grid-gap, 1em)*-1)}[class*="grid-gap"]>*{margin-bottom:1em;margin-bottom:var(--grid-gap, 1em);margin-right:1em;margin-right:var(--grid-gap, 1em)}.grid-gap-xxxxs{--grid-gap: var(--space-xxxxs)}.grid-gap-xxxs{--grid-gap: var(--space-xxxs)}.grid-gap-xxs{--grid-gap: var(--space-xxs)}.grid-gap-xs{--grid-gap: var(--space-xs)}.grid-gap-sm{--grid-gap: var(--space-sm)}.grid-gap-md{--grid-gap: var(--space-md)}.grid-gap-lg{--grid-gap: var(--space-lg)}.grid-gap-xl{--grid-gap: var(--space-xl)}.grid-gap-xxl{--grid-gap: var(--space-xxl)}.grid-gap-xxxl{--grid-gap: var(--space-xxxl)}.grid-gap-xxxxl{--grid-gap: var(--space-xxxxl)}.col{-ms-flex-positive:1;flex-grow:1;-ms-flex-preferred-size:0;flex-basis:0;max-width:100%}.col-1{-ms-flex-preferred-size:calc(8.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(8.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(8.33% - 0.01px - 1em);flex-basis:calc(8.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(8.33% - 0.01px - 1em);max-width:calc(8.33% - 0.01px - var(--grid-gap, 1em))}.col-2{-ms-flex-preferred-size:calc(16.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(16.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(16.66% - 0.01px - 1em);flex-basis:calc(16.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(16.66% - 0.01px - 1em);max-width:calc(16.66% - 0.01px - var(--grid-gap, 1em))}.col-3{-ms-flex-preferred-size:calc(25% - 0.01px - 1em);-ms-flex-preferred-size:calc(25% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(25% - 0.01px - 1em);flex-basis:calc(25% - 0.01px - var(--grid-gap, 1em));max-width:calc(25% - 0.01px - 1em);max-width:calc(25% - 0.01px - var(--grid-gap, 1em))}.col-4{-ms-flex-preferred-size:calc(33.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(33.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(33.33% - 0.01px - 1em);flex-basis:calc(33.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(33.33% - 0.01px - 1em);max-width:calc(33.33% - 0.01px - var(--grid-gap, 1em))}.col-5{-ms-flex-preferred-size:calc(41.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(41.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(41.66% - 0.01px - 1em);flex-basis:calc(41.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(41.66% - 0.01px - 1em);max-width:calc(41.66% - 0.01px - var(--grid-gap, 1em))}.col-6{-ms-flex-preferred-size:calc(50% - 0.01px - 1em);-ms-flex-preferred-size:calc(50% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(50% - 0.01px - 1em);flex-basis:calc(50% - 0.01px - var(--grid-gap, 1em));max-width:calc(50% - 0.01px - 1em);max-width:calc(50% - 0.01px - var(--grid-gap, 1em))}.col-7{-ms-flex-preferred-size:calc(58.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(58.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(58.33% - 0.01px - 1em);flex-basis:calc(58.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(58.33% - 0.01px - 1em);max-width:calc(58.33% - 0.01px - var(--grid-gap, 1em))}.col-8{-ms-flex-preferred-size:calc(66.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(66.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(66.66% - 0.01px - 1em);flex-basis:calc(66.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(66.66% - 0.01px - 1em);max-width:calc(66.66% - 0.01px - var(--grid-gap, 1em))}.col-9{-ms-flex-preferred-size:calc(75% - 0.01px - 1em);-ms-flex-preferred-size:calc(75% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(75% - 0.01px - 1em);flex-basis:calc(75% - 0.01px - var(--grid-gap, 1em));max-width:calc(75% - 0.01px - 1em);max-width:calc(75% - 0.01px - var(--grid-gap, 1em))}.col-10{-ms-flex-preferred-size:calc(83.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(83.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(83.33% - 0.01px - 1em);flex-basis:calc(83.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(83.33% - 0.01px - 1em);max-width:calc(83.33% - 0.01px - var(--grid-gap, 1em))}.col-11{-ms-flex-preferred-size:calc(91.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(91.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(91.66% - 0.01px - 1em);flex-basis:calc(91.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(91.66% - 0.01px - 1em);max-width:calc(91.66% - 0.01px - var(--grid-gap, 1em))}.col-12{-ms-flex-preferred-size:calc(100% - 0.01px - 1em);-ms-flex-preferred-size:calc(100% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(100% - 0.01px - 1em);flex-basis:calc(100% - 0.01px - var(--grid-gap, 1em));max-width:calc(100% - 0.01px - 1em);max-width:calc(100% - 0.01px - var(--grid-gap, 1em))}@media (min-width: 32rem){.col\@xs{-ms-flex-positive:1;flex-grow:1;-ms-flex-preferred-size:0;flex-basis:0;max-width:100%}.col-1\@xs{-ms-flex-preferred-size:calc(8.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(8.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(8.33% - 0.01px - 1em);flex-basis:calc(8.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(8.33% - 0.01px - 1em);max-width:calc(8.33% - 0.01px - var(--grid-gap, 1em))}.col-2\@xs{-ms-flex-preferred-size:calc(16.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(16.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(16.66% - 0.01px - 1em);flex-basis:calc(16.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(16.66% - 0.01px - 1em);max-width:calc(16.66% - 0.01px - var(--grid-gap, 1em))}.col-3\@xs{-ms-flex-preferred-size:calc(25% - 0.01px - 1em);-ms-flex-preferred-size:calc(25% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(25% - 0.01px - 1em);flex-basis:calc(25% - 0.01px - var(--grid-gap, 1em));max-width:calc(25% - 0.01px - 1em);max-width:calc(25% - 0.01px - var(--grid-gap, 1em))}.col-4\@xs{-ms-flex-preferred-size:calc(33.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(33.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(33.33% - 0.01px - 1em);flex-basis:calc(33.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(33.33% - 0.01px - 1em);max-width:calc(33.33% - 0.01px - var(--grid-gap, 1em))}.col-5\@xs{-ms-flex-preferred-size:calc(41.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(41.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(41.66% - 0.01px - 1em);flex-basis:calc(41.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(41.66% - 0.01px - 1em);max-width:calc(41.66% - 0.01px - var(--grid-gap, 1em))}.col-6\@xs{-ms-flex-preferred-size:calc(50% - 0.01px - 1em);-ms-flex-preferred-size:calc(50% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(50% - 0.01px - 1em);flex-basis:calc(50% - 0.01px - var(--grid-gap, 1em));max-width:calc(50% - 0.01px - 1em);max-width:calc(50% - 0.01px - var(--grid-gap, 1em))}.col-7\@xs{-ms-flex-preferred-size:calc(58.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(58.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(58.33% - 0.01px - 1em);flex-basis:calc(58.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(58.33% - 0.01px - 1em);max-width:calc(58.33% - 0.01px - var(--grid-gap, 1em))}.col-8\@xs{-ms-flex-preferred-size:calc(66.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(66.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(66.66% - 0.01px - 1em);flex-basis:calc(66.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(66.66% - 0.01px - 1em);max-width:calc(66.66% - 0.01px - var(--grid-gap, 1em))}.col-9\@xs{-ms-flex-preferred-size:calc(75% - 0.01px - 1em);-ms-flex-preferred-size:calc(75% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(75% - 0.01px - 1em);flex-basis:calc(75% - 0.01px - var(--grid-gap, 1em));max-width:calc(75% - 0.01px - 1em);max-width:calc(75% - 0.01px - var(--grid-gap, 1em))}.col-10\@xs{-ms-flex-preferred-size:calc(83.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(83.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(83.33% - 0.01px - 1em);flex-basis:calc(83.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(83.33% - 0.01px - 1em);max-width:calc(83.33% - 0.01px - var(--grid-gap, 1em))}.col-11\@xs{-ms-flex-preferred-size:calc(91.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(91.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(91.66% - 0.01px - 1em);flex-basis:calc(91.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(91.66% - 0.01px - 1em);max-width:calc(91.66% - 0.01px - var(--grid-gap, 1em))}.col-12\@xs{-ms-flex-preferred-size:calc(100% - 0.01px - 1em);-ms-flex-preferred-size:calc(100% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(100% - 0.01px - 1em);flex-basis:calc(100% - 0.01px - var(--grid-gap, 1em));max-width:calc(100% - 0.01px - 1em);max-width:calc(100% - 0.01px - var(--grid-gap, 1em))}}@media (min-width: 48rem){.col\@sm{-ms-flex-positive:1;flex-grow:1;-ms-flex-preferred-size:0;flex-basis:0;max-width:100%}.col-1\@sm{-ms-flex-preferred-size:calc(8.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(8.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(8.33% - 0.01px - 1em);flex-basis:calc(8.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(8.33% - 0.01px - 1em);max-width:calc(8.33% - 0.01px - var(--grid-gap, 1em))}.col-2\@sm{-ms-flex-preferred-size:calc(16.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(16.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(16.66% - 0.01px - 1em);flex-basis:calc(16.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(16.66% - 0.01px - 1em);max-width:calc(16.66% - 0.01px - var(--grid-gap, 1em))}.col-3\@sm{-ms-flex-preferred-size:calc(25% - 0.01px - 1em);-ms-flex-preferred-size:calc(25% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(25% - 0.01px - 1em);flex-basis:calc(25% - 0.01px - var(--grid-gap, 1em));max-width:calc(25% - 0.01px - 1em);max-width:calc(25% - 0.01px - var(--grid-gap, 1em))}.col-4\@sm{-ms-flex-preferred-size:calc(33.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(33.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(33.33% - 0.01px - 1em);flex-basis:calc(33.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(33.33% - 0.01px - 1em);max-width:calc(33.33% - 0.01px - var(--grid-gap, 1em))}.col-5\@sm{-ms-flex-preferred-size:calc(41.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(41.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(41.66% - 0.01px - 1em);flex-basis:calc(41.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(41.66% - 0.01px - 1em);max-width:calc(41.66% - 0.01px - var(--grid-gap, 1em))}.col-6\@sm{-ms-flex-preferred-size:calc(50% - 0.01px - 1em);-ms-flex-preferred-size:calc(50% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(50% - 0.01px - 1em);flex-basis:calc(50% - 0.01px - var(--grid-gap, 1em));max-width:calc(50% - 0.01px - 1em);max-width:calc(50% - 0.01px - var(--grid-gap, 1em))}.col-7\@sm{-ms-flex-preferred-size:calc(58.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(58.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(58.33% - 0.01px - 1em);flex-basis:calc(58.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(58.33% - 0.01px - 1em);max-width:calc(58.33% - 0.01px - var(--grid-gap, 1em))}.col-8\@sm{-ms-flex-preferred-size:calc(66.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(66.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(66.66% - 0.01px - 1em);flex-basis:calc(66.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(66.66% - 0.01px - 1em);max-width:calc(66.66% - 0.01px - var(--grid-gap, 1em))}.col-9\@sm{-ms-flex-preferred-size:calc(75% - 0.01px - 1em);-ms-flex-preferred-size:calc(75% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(75% - 0.01px - 1em);flex-basis:calc(75% - 0.01px - var(--grid-gap, 1em));max-width:calc(75% - 0.01px - 1em);max-width:calc(75% - 0.01px - var(--grid-gap, 1em))}.col-10\@sm{-ms-flex-preferred-size:calc(83.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(83.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(83.33% - 0.01px - 1em);flex-basis:calc(83.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(83.33% - 0.01px - 1em);max-width:calc(83.33% - 0.01px - var(--grid-gap, 1em))}.col-11\@sm{-ms-flex-preferred-size:calc(91.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(91.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(91.66% - 0.01px - 1em);flex-basis:calc(91.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(91.66% - 0.01px - 1em);max-width:calc(91.66% - 0.01px - var(--grid-gap, 1em))}.col-12\@sm{-ms-flex-preferred-size:calc(100% - 0.01px - 1em);-ms-flex-preferred-size:calc(100% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(100% - 0.01px - 1em);flex-basis:calc(100% - 0.01px - var(--grid-gap, 1em));max-width:calc(100% - 0.01px - 1em);max-width:calc(100% - 0.01px - var(--grid-gap, 1em))}}@media (min-width: 64rem){.col\@md{-ms-flex-positive:1;flex-grow:1;-ms-flex-preferred-size:0;flex-basis:0;max-width:100%}.col-1\@md{-ms-flex-preferred-size:calc(8.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(8.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(8.33% - 0.01px - 1em);flex-basis:calc(8.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(8.33% - 0.01px - 1em);max-width:calc(8.33% - 0.01px - var(--grid-gap, 1em))}.col-2\@md{-ms-flex-preferred-size:calc(16.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(16.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(16.66% - 0.01px - 1em);flex-basis:calc(16.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(16.66% - 0.01px - 1em);max-width:calc(16.66% - 0.01px - var(--grid-gap, 1em))}.col-3\@md{-ms-flex-preferred-size:calc(25% - 0.01px - 1em);-ms-flex-preferred-size:calc(25% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(25% - 0.01px - 1em);flex-basis:calc(25% - 0.01px - var(--grid-gap, 1em));max-width:calc(25% - 0.01px - 1em);max-width:calc(25% - 0.01px - var(--grid-gap, 1em))}.col-4\@md{-ms-flex-preferred-size:calc(33.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(33.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(33.33% - 0.01px - 1em);flex-basis:calc(33.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(33.33% - 0.01px - 1em);max-width:calc(33.33% - 0.01px - var(--grid-gap, 1em))}.col-5\@md{-ms-flex-preferred-size:calc(41.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(41.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(41.66% - 0.01px - 1em);flex-basis:calc(41.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(41.66% - 0.01px - 1em);max-width:calc(41.66% - 0.01px - var(--grid-gap, 1em))}.col-6\@md{-ms-flex-preferred-size:calc(50% - 0.01px - 1em);-ms-flex-preferred-size:calc(50% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(50% - 0.01px - 1em);flex-basis:calc(50% - 0.01px - var(--grid-gap, 1em));max-width:calc(50% - 0.01px - 1em);max-width:calc(50% - 0.01px - var(--grid-gap, 1em))}.col-7\@md{-ms-flex-preferred-size:calc(58.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(58.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(58.33% - 0.01px - 1em);flex-basis:calc(58.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(58.33% - 0.01px - 1em);max-width:calc(58.33% - 0.01px - var(--grid-gap, 1em))}.col-8\@md{-ms-flex-preferred-size:calc(66.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(66.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(66.66% - 0.01px - 1em);flex-basis:calc(66.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(66.66% - 0.01px - 1em);max-width:calc(66.66% - 0.01px - var(--grid-gap, 1em))}.col-9\@md{-ms-flex-preferred-size:calc(75% - 0.01px - 1em);-ms-flex-preferred-size:calc(75% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(75% - 0.01px - 1em);flex-basis:calc(75% - 0.01px - var(--grid-gap, 1em));max-width:calc(75% - 0.01px - 1em);max-width:calc(75% - 0.01px - var(--grid-gap, 1em))}.col-10\@md{-ms-flex-preferred-size:calc(83.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(83.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(83.33% - 0.01px - 1em);flex-basis:calc(83.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(83.33% - 0.01px - 1em);max-width:calc(83.33% - 0.01px - var(--grid-gap, 1em))}.col-11\@md{-ms-flex-preferred-size:calc(91.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(91.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(91.66% - 0.01px - 1em);flex-basis:calc(91.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(91.66% - 0.01px - 1em);max-width:calc(91.66% - 0.01px - var(--grid-gap, 1em))}.col-12\@md{-ms-flex-preferred-size:calc(100% - 0.01px - 1em);-ms-flex-preferred-size:calc(100% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(100% - 0.01px - 1em);flex-basis:calc(100% - 0.01px - var(--grid-gap, 1em));max-width:calc(100% - 0.01px - 1em);max-width:calc(100% - 0.01px - var(--grid-gap, 1em))}}@media (min-width: 80rem){.col\@lg{-ms-flex-positive:1;flex-grow:1;-ms-flex-preferred-size:0;flex-basis:0;max-width:100%}.col-1\@lg{-ms-flex-preferred-size:calc(8.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(8.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(8.33% - 0.01px - 1em);flex-basis:calc(8.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(8.33% - 0.01px - 1em);max-width:calc(8.33% - 0.01px - var(--grid-gap, 1em))}.col-2\@lg{-ms-flex-preferred-size:calc(16.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(16.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(16.66% - 0.01px - 1em);flex-basis:calc(16.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(16.66% - 0.01px - 1em);max-width:calc(16.66% - 0.01px - var(--grid-gap, 1em))}.col-3\@lg{-ms-flex-preferred-size:calc(25% - 0.01px - 1em);-ms-flex-preferred-size:calc(25% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(25% - 0.01px - 1em);flex-basis:calc(25% - 0.01px - var(--grid-gap, 1em));max-width:calc(25% - 0.01px - 1em);max-width:calc(25% - 0.01px - var(--grid-gap, 1em))}.col-4\@lg{-ms-flex-preferred-size:calc(33.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(33.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(33.33% - 0.01px - 1em);flex-basis:calc(33.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(33.33% - 0.01px - 1em);max-width:calc(33.33% - 0.01px - var(--grid-gap, 1em))}.col-5\@lg{-ms-flex-preferred-size:calc(41.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(41.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(41.66% - 0.01px - 1em);flex-basis:calc(41.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(41.66% - 0.01px - 1em);max-width:calc(41.66% - 0.01px - var(--grid-gap, 1em))}.col-6\@lg{-ms-flex-preferred-size:calc(50% - 0.01px - 1em);-ms-flex-preferred-size:calc(50% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(50% - 0.01px - 1em);flex-basis:calc(50% - 0.01px - var(--grid-gap, 1em));max-width:calc(50% - 0.01px - 1em);max-width:calc(50% - 0.01px - var(--grid-gap, 1em))}.col-7\@lg{-ms-flex-preferred-size:calc(58.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(58.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(58.33% - 0.01px - 1em);flex-basis:calc(58.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(58.33% - 0.01px - 1em);max-width:calc(58.33% - 0.01px - var(--grid-gap, 1em))}.col-8\@lg{-ms-flex-preferred-size:calc(66.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(66.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(66.66% - 0.01px - 1em);flex-basis:calc(66.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(66.66% - 0.01px - 1em);max-width:calc(66.66% - 0.01px - var(--grid-gap, 1em))}.col-9\@lg{-ms-flex-preferred-size:calc(75% - 0.01px - 1em);-ms-flex-preferred-size:calc(75% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(75% - 0.01px - 1em);flex-basis:calc(75% - 0.01px - var(--grid-gap, 1em));max-width:calc(75% - 0.01px - 1em);max-width:calc(75% - 0.01px - var(--grid-gap, 1em))}.col-10\@lg{-ms-flex-preferred-size:calc(83.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(83.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(83.33% - 0.01px - 1em);flex-basis:calc(83.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(83.33% - 0.01px - 1em);max-width:calc(83.33% - 0.01px - var(--grid-gap, 1em))}.col-11\@lg{-ms-flex-preferred-size:calc(91.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(91.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(91.66% - 0.01px - 1em);flex-basis:calc(91.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(91.66% - 0.01px - 1em);max-width:calc(91.66% - 0.01px - var(--grid-gap, 1em))}.col-12\@lg{-ms-flex-preferred-size:calc(100% - 0.01px - 1em);-ms-flex-preferred-size:calc(100% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(100% - 0.01px - 1em);flex-basis:calc(100% - 0.01px - var(--grid-gap, 1em));max-width:calc(100% - 0.01px - 1em);max-width:calc(100% - 0.01px - var(--grid-gap, 1em))}}@media (min-width: 90rem){.col\@xl{-ms-flex-positive:1;flex-grow:1;-ms-flex-preferred-size:0;flex-basis:0;max-width:100%}.col-1\@xl{-ms-flex-preferred-size:calc(8.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(8.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(8.33% - 0.01px - 1em);flex-basis:calc(8.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(8.33% - 0.01px - 1em);max-width:calc(8.33% - 0.01px - var(--grid-gap, 1em))}.col-2\@xl{-ms-flex-preferred-size:calc(16.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(16.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(16.66% - 0.01px - 1em);flex-basis:calc(16.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(16.66% - 0.01px - 1em);max-width:calc(16.66% - 0.01px - var(--grid-gap, 1em))}.col-3\@xl{-ms-flex-preferred-size:calc(25% - 0.01px - 1em);-ms-flex-preferred-size:calc(25% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(25% - 0.01px - 1em);flex-basis:calc(25% - 0.01px - var(--grid-gap, 1em));max-width:calc(25% - 0.01px - 1em);max-width:calc(25% - 0.01px - var(--grid-gap, 1em))}.col-4\@xl{-ms-flex-preferred-size:calc(33.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(33.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(33.33% - 0.01px - 1em);flex-basis:calc(33.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(33.33% - 0.01px - 1em);max-width:calc(33.33% - 0.01px - var(--grid-gap, 1em))}.col-5\@xl{-ms-flex-preferred-size:calc(41.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(41.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(41.66% - 0.01px - 1em);flex-basis:calc(41.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(41.66% - 0.01px - 1em);max-width:calc(41.66% - 0.01px - var(--grid-gap, 1em))}.col-6\@xl{-ms-flex-preferred-size:calc(50% - 0.01px - 1em);-ms-flex-preferred-size:calc(50% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(50% - 0.01px - 1em);flex-basis:calc(50% - 0.01px - var(--grid-gap, 1em));max-width:calc(50% - 0.01px - 1em);max-width:calc(50% - 0.01px - var(--grid-gap, 1em))}.col-7\@xl{-ms-flex-preferred-size:calc(58.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(58.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(58.33% - 0.01px - 1em);flex-basis:calc(58.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(58.33% - 0.01px - 1em);max-width:calc(58.33% - 0.01px - var(--grid-gap, 1em))}.col-8\@xl{-ms-flex-preferred-size:calc(66.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(66.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(66.66% - 0.01px - 1em);flex-basis:calc(66.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(66.66% - 0.01px - 1em);max-width:calc(66.66% - 0.01px - var(--grid-gap, 1em))}.col-9\@xl{-ms-flex-preferred-size:calc(75% - 0.01px - 1em);-ms-flex-preferred-size:calc(75% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(75% - 0.01px - 1em);flex-basis:calc(75% - 0.01px - var(--grid-gap, 1em));max-width:calc(75% - 0.01px - 1em);max-width:calc(75% - 0.01px - var(--grid-gap, 1em))}.col-10\@xl{-ms-flex-preferred-size:calc(83.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(83.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(83.33% - 0.01px - 1em);flex-basis:calc(83.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(83.33% - 0.01px - 1em);max-width:calc(83.33% - 0.01px - var(--grid-gap, 1em))}.col-11\@xl{-ms-flex-preferred-size:calc(91.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(91.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(91.66% - 0.01px - 1em);flex-basis:calc(91.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(91.66% - 0.01px - 1em);max-width:calc(91.66% - 0.01px - var(--grid-gap, 1em))}.col-12\@xl{-ms-flex-preferred-size:calc(100% - 0.01px - 1em);-ms-flex-preferred-size:calc(100% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(100% - 0.01px - 1em);flex-basis:calc(100% - 0.01px - var(--grid-gap, 1em));max-width:calc(100% - 0.01px - 1em);max-width:calc(100% - 0.01px - var(--grid-gap, 1em))}}:root{--radius-sm: calc(var(--radius, 0.25em)/2);--radius-md: var(--radius, 0.25em);--radius-lg: calc(var(--radius, 0.25em)*2);--shadow-sm: 0 1px 2px rgba(0, 0, 0, .085), 0 1px 8px rgba(0, 0, 0, .1);--shadow-md: 0 1px 8px rgba(0, 0, 0, .1), 0 8px 24px rgba(0, 0, 0, .15);--shadow-lg: 0 1px 8px rgba(0, 0, 0, .1), 0 16px 48px rgba(0, 0, 0, .1), 0 24px 60px rgba(0, 0, 0, .1);--bounce: cubic-bezier(0.175, 0.885, 0.32, 1.275);--ease-in-out: cubic-bezier(0.645, 0.045, 0.355, 1);--ease-in: cubic-bezier(0.55, 0.055, 0.675, 0.19);--ease-out: cubic-bezier(0.215, 0.61, 0.355, 1)}:root{--body-line-height: 1.4;--heading-line-height: 1.2}body{font-size:1em;font-size:var(--text-base-size, 1em);font-family:'Droid Serif', serif;font-family:var(--font-primary, sans-serif);color:hsl(240, 4%, 20%);color:var(--color-contrast-high, #313135)}h1,h2,h3,h4{color:hsl(240, 8%, 12%);color:var(--color-contrast-higher, #1c1c21);line-height:1.2;line-height:var(--heading-line-height, 1.2)}.text-xxxl{font-size:2.48832em;font-size:var(--text-xxxl, 2.488em)}h1,.text-xxl{font-size:2.0736em;font-size:var(--text-xxl, 2.074em)}h2,.text-xl{font-size:1.728em;font-size:var(--text-xl, 1.728em)}h3,.text-lg{font-size:1.44em;font-size:var(--text-lg, 1.44em)}h4,.text-md{font-size:1.2em;font-size:var(--text-md, 1.2em)}small,.text-sm{font-size:0.83333em;font-size:var(--text-sm, 0.833em)}.text-xs{font-size:0.69444em;font-size:var(--text-xs, 0.694em)}a,.link{color:hsl(220, 90%, 56%);color:var(--color-primary, #2a6df4);text-decoration:underline}strong,.text-bold{font-weight:bold}s{text-decoration:line-through}u,.text-underline{text-decoration:underline}.text-component{--component-body-line-height: calc(var(--body-line-height)*var(--line-height-multiplier, 1));--component-heading-line-height: calc(var(--heading-line-height)*var(--line-height-multiplier, 1))}.text-component h1,.text-component h2,.text-component h3,.text-component h4{line-height:1.2;line-height:var(--component-heading-line-height, 1.2);margin-bottom:0.25em;margin-bottom:calc(var(--space-xxxs)*var(--text-vspace-multiplier, 1))}.text-component h2,.text-component h3,.text-component h4{margin-top:0.75em;margin-top:calc(var(--space-sm)*var(--text-vspace-multiplier, 1))}.text-component p,.text-component blockquote,.text-component ul li,.text-component ol li{line-height:1.4;line-height:var(--component-body-line-height)}.text-component ul,.text-component ol,.text-component p,.text-component blockquote,.text-component .text-component__block{margin-bottom:0.75em;margin-bottom:calc(var(--space-sm)*var(--text-vspace-multiplier, 1))}.text-component ul,.text-component ol{padding-left:1em}.text-component ul{list-style-type:disc}.text-component ol{list-style-type:decimal}.text-component img{display:block;margin:0 auto}.text-component figcaption{text-align:center;margin-top:0.5em;margin-top:var(--space-xs)}.text-component em{font-style:italic}.text-component hr{margin-top:2em;margin-top:calc(var(--space-lg)*var(--text-vspace-multiplier, 1));margin-bottom:2em;margin-bottom:calc(var(--space-lg)*var(--text-vspace-multiplier, 1));margin-left:auto;margin-right:auto}.text-component>*:first-child{margin-top:0}.text-component>*:last-child{margin-bottom:0}.text-component__block--full-width{width:100vw;margin-left:calc(50% - 50vw)}@media (min-width: 48rem){.text-component__block--left,.text-component__block--right{width:45%}.text-component__block--left img,.text-component__block--right img{width:100%}.text-component__block--left{float:left;margin-right:0.75em;margin-right:calc(var(--space-sm)*var(--text-vspace-multiplier, 1))}.text-component__block--right{float:right;margin-left:0.75em;margin-left:calc(var(--space-sm)*var(--text-vspace-multiplier, 1))}}@media (min-width: 90rem){.text-component__block--outset{width:calc(100% + 10.5em);width:calc(100% + 2*var(--space-xxl))}.text-component__block--outset img{width:100%}.text-component__block--outset:not(.text-component__block--right){margin-left:-5.25em;margin-left:calc(-1*var(--space-xxl))}.text-component__block--left,.text-component__block--right{width:50%}.text-component__block--right.text-component__block--outset{margin-right:-5.25em;margin-right:calc(-1*var(--space-xxl))}}:root{--icon-xxs: 12px;--icon-xs: 16px;--icon-sm: 24px;--icon-md: 32px;--icon-lg: 48px;--icon-xl: 64px;--icon-xxl: 128px}.icon{display:inline-block;color:inherit;fill:currentColor;height:1em;width:1em;line-height:1;-ms-flex-negative:0;flex-shrink:0}.icon--xxs{font-size:12px;font-size:var(--icon-xxs)}.icon--xs{font-size:16px;font-size:var(--icon-xs)}.icon--sm{font-size:24px;font-size:var(--icon-sm)}.icon--md{font-size:32px;font-size:var(--icon-md)}.icon--lg{font-size:48px;font-size:var(--icon-lg)}.icon--xl{font-size:64px;font-size:var(--icon-xl)}.icon--xxl{font-size:128px;font-size:var(--icon-xxl)}.icon--is-spinning{-webkit-animation:icon-spin 1s infinite linear;animation:icon-spin 1s infinite linear}@-webkit-keyframes icon-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes icon-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.icon use{color:inherit;fill:currentColor}.btn{position:relative;display:-ms-inline-flexbox;display:inline-flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center;white-space:nowrap;text-decoration:none;line-height:1;font-size:1em;font-size:var(--btn-font-size, 1em);padding-top:0.5em;padding-top:var(--btn-padding-y, 0.5em);padding-bottom:0.5em;padding-bottom:var(--btn-padding-y, 0.5em);padding-left:0.75em;padding-left:var(--btn-padding-x, 0.75em);padding-right:0.75em;padding-right:var(--btn-padding-x, 0.75em);border-radius:0.25em;border-radius:var(--btn-radius, 0.25em)}.btn--primary{background-color:hsl(220, 90%, 56%);background-color:var(--color-primary, #2a6df4);color:hsl(0, 0%, 100%);color:var(--color-white, #fff)}.btn--subtle{background-color:hsl(240, 1%, 83%);background-color:var(--color-contrast-low, #d3d3d4);color:hsl(240, 8%, 12%);color:var(--color-contrast-higher, #1c1c21)}.btn--accent{background-color:hsl(355, 90%, 61%);background-color:var(--color-accent, #f54251);color:hsl(0, 0%, 100%);color:var(--color-white, #fff)}.btn--disabled{cursor:not-allowed}.btn--sm{font-size:0.8em;font-size:var(--btn-font-size-sm, 0.8em)}.btn--md{font-size:1.2em;font-size:var(--btn-font-size-md, 1.2em)}.btn--lg{font-size:1.4em;font-size:var(--btn-font-size-lg, 1.4em)}.btn--icon{padding:0.5em;padding:var(--btn-padding-y, 0.5em)}.form-control{background-color:hsl(0, 0%, 100%);background-color:var(--color-bg, #f2f2f2);padding-top:0.5em;padding-top:var(--form-control-padding-y, 0.5em);padding-bottom:0.5em;padding-bottom:var(--form-control-padding-y, 0.5em);padding-left:0.75em;padding-left:var(--form-control-padding-x, 0.75em);padding-right:0.75em;padding-right:var(--form-control-padding-x, 0.75em);border-radius:0.25em;border-radius:var(--form-control-radius, 0.25em)}.form-control::-webkit-input-placeholder{color:hsl(240, 1%, 48%);color:var(--color-contrast-medium, #79797c)}.form-control::-moz-placeholder{opacity:1;color:hsl(240, 1%, 48%);color:var(--color-contrast-medium, #79797c)}.form-control:-ms-input-placeholder{color:hsl(240, 1%, 48%);color:var(--color-contrast-medium, #79797c)}.form-control:-moz-placeholder{color:hsl(240, 1%, 48%);color:var(--color-contrast-medium, #79797c)}.form-control[disabled],.form-control[readonly]{cursor:not-allowed}.form-legend{color:hsl(240, 8%, 12%);color:var(--color-contrast-higher, #1c1c21);line-height:1.2;font-size:1.2em;font-size:var(--text-md, 1.2em);margin-bottom:0.375em;margin-bottom:var(--space-xxs)}.form-label{display:inline-block}.form__msg-error{background-color:hsl(355, 90%, 61%);background-color:var(--color-error, #f54251);color:hsl(0, 0%, 100%);color:var(--color-white, #fff);font-size:0.83333em;font-size:var(--text-sm, 0.833em);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;padding:0.5em;padding:var(--space-xs);margin-top:0.75em;margin-top:var(--space-sm);border-radius:0.25em;border-radius:var(--radius-md, 0.25em);position:absolute;clip:rect(1px, 1px, 1px, 1px)}.form__msg-error::before{content:'';position:absolute;left:0.75em;left:var(--space-sm);top:0;-webkit-transform:translateY(-100%);-ms-transform:translateY(-100%);transform:translateY(-100%);width:0;height:0;border:8px solid transparent;border-bottom-color:hsl(355, 90%, 61%);border-bottom-color:var(--color-error)}.form__msg-error--is-visible{position:relative;clip:auto}.radio-list>*,.checkbox-list>*{position:relative;display:-ms-flexbox;display:flex;-ms-flex-align:baseline;align-items:baseline;margin-bottom:0.375em;margin-bottom:var(--space-xxs)}.radio-list>*:last-of-type,.checkbox-list>*:last-of-type{margin-bottom:0}.radio-list label,.checkbox-list label{line-height:1.4;line-height:var(--body-line-height);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.radio-list input,.checkbox-list input{vertical-align:top;margin-right:0.25em;margin-right:var(--space-xxxs);-ms-flex-negative:0;flex-shrink:0}:root{--zindex-header: 2;--zindex-popover: 5;--zindex-fixed-element: 10;--zindex-overlay: 15}@media not all and (min-width: 32rem){.display\@xs{display:none !important}}@media (min-width: 32rem){.hide\@xs{display:none !important}}@media not all and (min-width: 48rem){.display\@sm{display:none !important}}@media (min-width: 48rem){.hide\@sm{display:none !important}}@media not all and (min-width: 64rem){.display\@md{display:none !important}}@media (min-width: 64rem){.hide\@md{display:none !important}}@media not all and (min-width: 80rem){.display\@lg{display:none !important}}@media (min-width: 80rem){.hide\@lg{display:none !important}}@media not all and (min-width: 90rem){.display\@xl{display:none !important}}@media (min-width: 90rem){.hide\@xl{display:none !important}}:root{--display: block}.is-visible{display:block !important;display:var(--display) !important}.is-hidden{display:none !important}.sr-only{position:absolute;clip:rect(1px, 1px, 1px, 1px);-webkit-clip-path:inset(50%);clip-path:inset(50%);width:1px;height:1px;overflow:hidden;padding:0;border:0;white-space:nowrap}.flex{display:-ms-flexbox;display:flex}.inline-flex{display:-ms-inline-flexbox;display:inline-flex}.flex-wrap{-ms-flex-wrap:wrap;flex-wrap:wrap}.flex-column{-ms-flex-direction:column;flex-direction:column}.flex-row{-ms-flex-direction:row;flex-direction:row}.flex-center{-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center}.justify-start{-ms-flex-pack:start;justify-content:flex-start}.justify-end{-ms-flex-pack:end;justify-content:flex-end}.justify-center{-ms-flex-pack:center;justify-content:center}.justify-between{-ms-flex-pack:justify;justify-content:space-between}.items-center{-ms-flex-align:center;align-items:center}.items-start{-ms-flex-align:start;align-items:flex-start}.items-end{-ms-flex-align:end;align-items:flex-end}@media (min-width: 32rem){.flex-wrap\@xs{-ms-flex-wrap:wrap;flex-wrap:wrap}.flex-column\@xs{-ms-flex-direction:column;flex-direction:column}.flex-row\@xs{-ms-flex-direction:row;flex-direction:row}.flex-center\@xs{-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center}.justify-start\@xs{-ms-flex-pack:start;justify-content:flex-start}.justify-end\@xs{-ms-flex-pack:end;justify-content:flex-end}.justify-center\@xs{-ms-flex-pack:center;justify-content:center}.justify-between\@xs{-ms-flex-pack:justify;justify-content:space-between}.items-center\@xs{-ms-flex-align:center;align-items:center}.items-start\@xs{-ms-flex-align:start;align-items:flex-start}.items-end\@xs{-ms-flex-align:end;align-items:flex-end}}@media (min-width: 48rem){.flex-wrap\@sm{-ms-flex-wrap:wrap;flex-wrap:wrap}.flex-column\@sm{-ms-flex-direction:column;flex-direction:column}.flex-row\@sm{-ms-flex-direction:row;flex-direction:row}.flex-center\@sm{-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center}.justify-start\@sm{-ms-flex-pack:start;justify-content:flex-start}.justify-end\@sm{-ms-flex-pack:end;justify-content:flex-end}.justify-center\@sm{-ms-flex-pack:center;justify-content:center}.justify-between\@sm{-ms-flex-pack:justify;justify-content:space-between}.items-center\@sm{-ms-flex-align:center;align-items:center}.items-start\@sm{-ms-flex-align:start;align-items:flex-start}.items-end\@sm{-ms-flex-align:end;align-items:flex-end}}@media (min-width: 64rem){.flex-wrap\@md{-ms-flex-wrap:wrap;flex-wrap:wrap}.flex-column\@md{-ms-flex-direction:column;flex-direction:column}.flex-row\@md{-ms-flex-direction:row;flex-direction:row}.flex-center\@md{-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center}.justify-start\@md{-ms-flex-pack:start;justify-content:flex-start}.justify-end\@md{-ms-flex-pack:end;justify-content:flex-end}.justify-center\@md{-ms-flex-pack:center;justify-content:center}.justify-between\@md{-ms-flex-pack:justify;justify-content:space-between}.items-center\@md{-ms-flex-align:center;align-items:center}.items-start\@md{-ms-flex-align:start;align-items:flex-start}.items-end\@md{-ms-flex-align:end;align-items:flex-end}}@media (min-width: 80rem){.flex-wrap\@lg{-ms-flex-wrap:wrap;flex-wrap:wrap}.flex-column\@lg{-ms-flex-direction:column;flex-direction:column}.flex-row\@lg{-ms-flex-direction:row;flex-direction:row}.flex-center\@lg{-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center}.justify-start\@lg{-ms-flex-pack:start;justify-content:flex-start}.justify-end\@lg{-ms-flex-pack:end;justify-content:flex-end}.justify-center\@lg{-ms-flex-pack:center;justify-content:center}.justify-between\@lg{-ms-flex-pack:justify;justify-content:space-between}.items-center\@lg{-ms-flex-align:center;align-items:center}.items-start\@lg{-ms-flex-align:start;align-items:flex-start}.items-end\@lg{-ms-flex-align:end;align-items:flex-end}}@media (min-width: 90rem){.flex-wrap\@xl{-ms-flex-wrap:wrap;flex-wrap:wrap}.flex-column\@xl{-ms-flex-direction:column;flex-direction:column}.flex-row\@xl{-ms-flex-direction:row;flex-direction:row}.flex-center\@xl{-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center}.justify-start\@xl{-ms-flex-pack:start;justify-content:flex-start}.justify-end\@xl{-ms-flex-pack:end;justify-content:flex-end}.justify-center\@xl{-ms-flex-pack:center;justify-content:center}.justify-between\@xl{-ms-flex-pack:justify;justify-content:space-between}.items-center\@xl{-ms-flex-align:center;align-items:center}.items-start\@xl{-ms-flex-align:start;align-items:flex-start}.items-end\@xl{-ms-flex-align:end;align-items:flex-end}}.flex-grow{-ms-flex-positive:1;flex-grow:1}.flex-shrink-0{-ms-flex-negative:0;flex-shrink:0}.flex-gap-xxxs{margin-bottom:-0.25em;margin-bottom:calc(-1*var(--space-xxxs));margin-right:-0.25em;margin-right:calc(-1*var(--space-xxxs))}.flex-gap-xxxs>*{margin-bottom:0.25em;margin-bottom:var(--space-xxxs);margin-right:0.25em;margin-right:var(--space-xxxs)}.flex-gap-xxs{margin-bottom:-0.375em;margin-bottom:calc(-1*var(--space-xxs));margin-right:-0.375em;margin-right:calc(-1*var(--space-xxs))}.flex-gap-xxs>*{margin-bottom:0.375em;margin-bottom:var(--space-xxs);margin-right:0.375em;margin-right:var(--space-xxs)}.flex-gap-xs{margin-bottom:-0.5em;margin-bottom:calc(-1*var(--space-xs));margin-right:-0.5em;margin-right:calc(-1*var(--space-xs))}.flex-gap-xs>*{margin-bottom:0.5em;margin-bottom:var(--space-xs);margin-right:0.5em;margin-right:var(--space-xs)}.flex-gap-sm{margin-bottom:-0.75em;margin-bottom:calc(-1*var(--space-sm));margin-right:-0.75em;margin-right:calc(-1*var(--space-sm))}.flex-gap-sm>*{margin-bottom:0.75em;margin-bottom:var(--space-sm);margin-right:0.75em;margin-right:var(--space-sm)}.flex-gap-md{margin-bottom:-1.25em;margin-bottom:calc(-1*var(--space-md));margin-right:-1.25em;margin-right:calc(-1*var(--space-md))}.flex-gap-md>*{margin-bottom:1.25em;margin-bottom:var(--space-md);margin-right:1.25em;margin-right:var(--space-md)}.flex-gap-lg{margin-bottom:-2em;margin-bottom:calc(-1*var(--space-lg));margin-right:-2em;margin-right:calc(-1*var(--space-lg))}.flex-gap-lg>*{margin-bottom:2em;margin-bottom:var(--space-lg);margin-right:2em;margin-right:var(--space-lg)}.flex-gap-xl{margin-bottom:-3.25em;margin-bottom:calc(-1*var(--space-xl));margin-right:-3.25em;margin-right:calc(-1*var(--space-xl))}.flex-gap-xl>*{margin-bottom:3.25em;margin-bottom:var(--space-xl);margin-right:3.25em;margin-right:var(--space-xl)}.flex-gap-xxl{margin-bottom:-5.25em;margin-bottom:calc(-1*var(--space-xxl));margin-right:-5.25em;margin-right:calc(-1*var(--space-xxl))}.flex-gap-xxl>*{margin-bottom:5.25em;margin-bottom:var(--space-xxl);margin-right:5.25em;margin-right:var(--space-xxl)}.margin-xxxxs{margin:0.125em;margin:var(--space-xxxxs)}.margin-xxxs{margin:0.25em;margin:var(--space-xxxs)}.margin-xxs{margin:0.375em;margin:var(--space-xxs)}.margin-xs{margin:0.5em;margin:var(--space-xs)}.margin-sm{margin:0.75em;margin:var(--space-sm)}.margin-md{margin:1.25em;margin:var(--space-md)}.margin-lg{margin:2em;margin:var(--space-lg)}.margin-xl{margin:3.25em;margin:var(--space-xl)}.margin-xxl{margin:5.25em;margin:var(--space-xxl)}.margin-xxxl{margin:8.5em;margin:var(--space-xxxl)}.margin-xxxxl{margin:13.75em;margin:var(--space-xxxxl)}.margin-auto{margin:auto}.margin-top-xxxxs{margin-top:0.125em;margin-top:var(--space-xxxxs)}.margin-top-xxxs{margin-top:0.25em;margin-top:var(--space-xxxs)}.margin-top-xxs{margin-top:0.375em;margin-top:var(--space-xxs)}.margin-top-xs{margin-top:0.5em;margin-top:var(--space-xs)}.margin-top-sm{margin-top:0.75em;margin-top:var(--space-sm)}.margin-top-md{margin-top:1.25em;margin-top:var(--space-md)}.margin-top-lg{margin-top:2em;margin-top:var(--space-lg)}.margin-top-xl{margin-top:3.25em;margin-top:var(--space-xl)}.margin-top-xxl{margin-top:5.25em;margin-top:var(--space-xxl)}.margin-top-xxxl{margin-top:8.5em;margin-top:var(--space-xxxl)}.margin-top-xxxxl{margin-top:13.75em;margin-top:var(--space-xxxxl)}.margin-top-auto{margin-top:auto}.margin-bottom-xxxxs{margin-bottom:0.125em;margin-bottom:var(--space-xxxxs)}.margin-bottom-xxxs{margin-bottom:0.25em;margin-bottom:var(--space-xxxs)}.margin-bottom-xxs{margin-bottom:0.375em;margin-bottom:var(--space-xxs)}.margin-bottom-xs{margin-bottom:0.5em;margin-bottom:var(--space-xs)}.margin-bottom-sm{margin-bottom:0.75em;margin-bottom:var(--space-sm)}.margin-bottom-md{margin-bottom:1.25em;margin-bottom:var(--space-md)}.margin-bottom-lg{margin-bottom:2em;margin-bottom:var(--space-lg)}.margin-bottom-xl{margin-bottom:3.25em;margin-bottom:var(--space-xl)}.margin-bottom-xxl{margin-bottom:5.25em;margin-bottom:var(--space-xxl)}.margin-bottom-xxxl{margin-bottom:8.5em;margin-bottom:var(--space-xxxl)}.margin-bottom-xxxxl{margin-bottom:13.75em;margin-bottom:var(--space-xxxxl)}.margin-bottom-auto{margin-bottom:auto}.margin-right-xxxxs{margin-right:0.125em;margin-right:var(--space-xxxxs)}.margin-right-xxxs{margin-right:0.25em;margin-right:var(--space-xxxs)}.margin-right-xxs{margin-right:0.375em;margin-right:var(--space-xxs)}.margin-right-xs{margin-right:0.5em;margin-right:var(--space-xs)}.margin-right-sm{margin-right:0.75em;margin-right:var(--space-sm)}.margin-right-md{margin-right:1.25em;margin-right:var(--space-md)}.margin-right-lg{margin-right:2em;margin-right:var(--space-lg)}.margin-right-xl{margin-right:3.25em;margin-right:var(--space-xl)}.margin-right-xxl{margin-right:5.25em;margin-right:var(--space-xxl)}.margin-right-xxxl{margin-right:8.5em;margin-right:var(--space-xxxl)}.margin-right-xxxxl{margin-right:13.75em;margin-right:var(--space-xxxxl)}.margin-right-auto{margin-right:auto}.margin-left-xxxxs{margin-left:0.125em;margin-left:var(--space-xxxxs)}.margin-left-xxxs{margin-left:0.25em;margin-left:var(--space-xxxs)}.margin-left-xxs{margin-left:0.375em;margin-left:var(--space-xxs)}.margin-left-xs{margin-left:0.5em;margin-left:var(--space-xs)}.margin-left-sm{margin-left:0.75em;margin-left:var(--space-sm)}.margin-left-md{margin-left:1.25em;margin-left:var(--space-md)}.margin-left-lg{margin-left:2em;margin-left:var(--space-lg)}.margin-left-xl{margin-left:3.25em;margin-left:var(--space-xl)}.margin-left-xxl{margin-left:5.25em;margin-left:var(--space-xxl)}.margin-left-xxxl{margin-left:8.5em;margin-left:var(--space-xxxl)}.margin-left-xxxxl{margin-left:13.75em;margin-left:var(--space-xxxxl)}.margin-left-auto{margin-left:auto}.margin-x-xxxxs{margin-left:0.125em;margin-left:var(--space-xxxxs);margin-right:0.125em;margin-right:var(--space-xxxxs)}.margin-x-xxxs{margin-left:0.25em;margin-left:var(--space-xxxs);margin-right:0.25em;margin-right:var(--space-xxxs)}.margin-x-xxs{margin-left:0.375em;margin-left:var(--space-xxs);margin-right:0.375em;margin-right:var(--space-xxs)}.margin-x-xs{margin-left:0.5em;margin-left:var(--space-xs);margin-right:0.5em;margin-right:var(--space-xs)}.margin-x-sm{margin-left:0.75em;margin-left:var(--space-sm);margin-right:0.75em;margin-right:var(--space-sm)}.margin-x-md{margin-left:1.25em;margin-left:var(--space-md);margin-right:1.25em;margin-right:var(--space-md)}.margin-x-lg{margin-left:2em;margin-left:var(--space-lg);margin-right:2em;margin-right:var(--space-lg)}.margin-x-xl{margin-left:3.25em;margin-left:var(--space-xl);margin-right:3.25em;margin-right:var(--space-xl)}.margin-x-xxl{margin-left:5.25em;margin-left:var(--space-xxl);margin-right:5.25em;margin-right:var(--space-xxl)}.margin-x-xxxl{margin-left:8.5em;margin-left:var(--space-xxxl);margin-right:8.5em;margin-right:var(--space-xxxl)}.margin-x-xxxxl{margin-left:13.75em;margin-left:var(--space-xxxxl);margin-right:13.75em;margin-right:var(--space-xxxxl)}.margin-x-auto{margin-left:auto;margin-right:auto}.margin-y-xxxxs{margin-top:0.125em;margin-top:var(--space-xxxxs);margin-bottom:0.125em;margin-bottom:var(--space-xxxxs)}.margin-y-xxxs{margin-top:0.25em;margin-top:var(--space-xxxs);margin-bottom:0.25em;margin-bottom:var(--space-xxxs)}.margin-y-xxs{margin-top:0.375em;margin-top:var(--space-xxs);margin-bottom:0.375em;margin-bottom:var(--space-xxs)}.margin-y-xs{margin-top:0.5em;margin-top:var(--space-xs);margin-bottom:0.5em;margin-bottom:var(--space-xs)}.margin-y-sm{margin-top:0.75em;margin-top:var(--space-sm);margin-bottom:0.75em;margin-bottom:var(--space-sm)}.margin-y-md{margin-top:1.25em;margin-top:var(--space-md);margin-bottom:1.25em;margin-bottom:var(--space-md)}.margin-y-lg{margin-top:2em;margin-top:var(--space-lg);margin-bottom:2em;margin-bottom:var(--space-lg)}.margin-y-xl{margin-top:3.25em;margin-top:var(--space-xl);margin-bottom:3.25em;margin-bottom:var(--space-xl)}.margin-y-xxl{margin-top:5.25em;margin-top:var(--space-xxl);margin-bottom:5.25em;margin-bottom:var(--space-xxl)}.margin-y-xxxl{margin-top:8.5em;margin-top:var(--space-xxxl);margin-bottom:8.5em;margin-bottom:var(--space-xxxl)}.margin-y-xxxxl{margin-top:13.75em;margin-top:var(--space-xxxxl);margin-bottom:13.75em;margin-bottom:var(--space-xxxxl)}.margin-y-auto{margin-top:auto;margin-bottom:auto}@media not all and (min-width: 32rem){.has-margin\@xs{margin:0 !important}}@media not all and (min-width: 48rem){.has-margin\@sm{margin:0 !important}}@media not all and (min-width: 64rem){.has-margin\@md{margin:0 !important}}@media not all and (min-width: 80rem){.has-margin\@lg{margin:0 !important}}@media not all and (min-width: 90rem){.has-margin\@xl{margin:0 !important}}.padding-md{padding:1.25em;padding:var(--space-md)}.padding-xxxxs{padding:0.125em;padding:var(--space-xxxxs)}.padding-xxxs{padding:0.25em;padding:var(--space-xxxs)}.padding-xxs{padding:0.375em;padding:var(--space-xxs)}.padding-xs{padding:0.5em;padding:var(--space-xs)}.padding-sm{padding:0.75em;padding:var(--space-sm)}.padding-lg{padding:2em;padding:var(--space-lg)}.padding-xl{padding:3.25em;padding:var(--space-xl)}.padding-xxl{padding:5.25em;padding:var(--space-xxl)}.padding-xxxl{padding:8.5em;padding:var(--space-xxxl)}.padding-xxxxl{padding:13.75em;padding:var(--space-xxxxl)}.padding-component{padding:1.25em;padding:var(--component-padding)}.padding-top-md{padding-top:1.25em;padding-top:var(--space-md)}.padding-top-xxxxs{padding-top:0.125em;padding-top:var(--space-xxxxs)}.padding-top-xxxs{padding-top:0.25em;padding-top:var(--space-xxxs)}.padding-top-xxs{padding-top:0.375em;padding-top:var(--space-xxs)}.padding-top-xs{padding-top:0.5em;padding-top:var(--space-xs)}.padding-top-sm{padding-top:0.75em;padding-top:var(--space-sm)}.padding-top-lg{padding-top:2em;padding-top:var(--space-lg)}.padding-top-xl{padding-top:3.25em;padding-top:var(--space-xl)}.padding-top-xxl{padding-top:5.25em;padding-top:var(--space-xxl)}.padding-top-xxxl{padding-top:8.5em;padding-top:var(--space-xxxl)}.padding-top-xxxxl{padding-top:13.75em;padding-top:var(--space-xxxxl)}.padding-top-component{padding-top:1.25em;padding-top:var(--component-padding)}.padding-bottom-md{padding-bottom:1.25em;padding-bottom:var(--space-md)}.padding-bottom-xxxxs{padding-bottom:0.125em;padding-bottom:var(--space-xxxxs)}.padding-bottom-xxxs{padding-bottom:0.25em;padding-bottom:var(--space-xxxs)}.padding-bottom-xxs{padding-bottom:0.375em;padding-bottom:var(--space-xxs)}.padding-bottom-xs{padding-bottom:0.5em;padding-bottom:var(--space-xs)}.padding-bottom-sm{padding-bottom:0.75em;padding-bottom:var(--space-sm)}.padding-bottom-lg{padding-bottom:2em;padding-bottom:var(--space-lg)}.padding-bottom-xl{padding-bottom:3.25em;padding-bottom:var(--space-xl)}.padding-bottom-xxl{padding-bottom:5.25em;padding-bottom:var(--space-xxl)}.padding-bottom-xxxl{padding-bottom:8.5em;padding-bottom:var(--space-xxxl)}.padding-bottom-xxxxl{padding-bottom:13.75em;padding-bottom:var(--space-xxxxl)}.padding-bottom-component{padding-bottom:1.25em;padding-bottom:var(--component-padding)}.padding-right-md{padding-right:1.25em;padding-right:var(--space-md)}.padding-right-xxxxs{padding-right:0.125em;padding-right:var(--space-xxxxs)}.padding-right-xxxs{padding-right:0.25em;padding-right:var(--space-xxxs)}.padding-right-xxs{padding-right:0.375em;padding-right:var(--space-xxs)}.padding-right-xs{padding-right:0.5em;padding-right:var(--space-xs)}.padding-right-sm{padding-right:0.75em;padding-right:var(--space-sm)}.padding-right-lg{padding-right:2em;padding-right:var(--space-lg)}.padding-right-xl{padding-right:3.25em;padding-right:var(--space-xl)}.padding-right-xxl{padding-right:5.25em;padding-right:var(--space-xxl)}.padding-right-xxxl{padding-right:8.5em;padding-right:var(--space-xxxl)}.padding-right-xxxxl{padding-right:13.75em;padding-right:var(--space-xxxxl)}.padding-right-component{padding-right:1.25em;padding-right:var(--component-padding)}.padding-left-md{padding-left:1.25em;padding-left:var(--space-md)}.padding-left-xxxxs{padding-left:0.125em;padding-left:var(--space-xxxxs)}.padding-left-xxxs{padding-left:0.25em;padding-left:var(--space-xxxs)}.padding-left-xxs{padding-left:0.375em;padding-left:var(--space-xxs)}.padding-left-xs{padding-left:0.5em;padding-left:var(--space-xs)}.padding-left-sm{padding-left:0.75em;padding-left:var(--space-sm)}.padding-left-lg{padding-left:2em;padding-left:var(--space-lg)}.padding-left-xl{padding-left:3.25em;padding-left:var(--space-xl)}.padding-left-xxl{padding-left:5.25em;padding-left:var(--space-xxl)}.padding-left-xxxl{padding-left:8.5em;padding-left:var(--space-xxxl)}.padding-left-xxxxl{padding-left:13.75em;padding-left:var(--space-xxxxl)}.padding-left-component{padding-left:1.25em;padding-left:var(--component-padding)}.padding-x-md{padding-left:1.25em;padding-left:var(--space-md);padding-right:1.25em;padding-right:var(--space-md)}.padding-x-xxxxs{padding-left:0.125em;padding-left:var(--space-xxxxs);padding-right:0.125em;padding-right:var(--space-xxxxs)}.padding-x-xxxs{padding-left:0.25em;padding-left:var(--space-xxxs);padding-right:0.25em;padding-right:var(--space-xxxs)}.padding-x-xxs{padding-left:0.375em;padding-left:var(--space-xxs);padding-right:0.375em;padding-right:var(--space-xxs)}.padding-x-xs{padding-left:0.5em;padding-left:var(--space-xs);padding-right:0.5em;padding-right:var(--space-xs)}.padding-x-sm{padding-left:0.75em;padding-left:var(--space-sm);padding-right:0.75em;padding-right:var(--space-sm)}.padding-x-lg{padding-left:2em;padding-left:var(--space-lg);padding-right:2em;padding-right:var(--space-lg)}.padding-x-xl{padding-left:3.25em;padding-left:var(--space-xl);padding-right:3.25em;padding-right:var(--space-xl)}.padding-x-xxl{padding-left:5.25em;padding-left:var(--space-xxl);padding-right:5.25em;padding-right:var(--space-xxl)}.padding-x-xxxl{padding-left:8.5em;padding-left:var(--space-xxxl);padding-right:8.5em;padding-right:var(--space-xxxl)}.padding-x-xxxxl{padding-left:13.75em;padding-left:var(--space-xxxxl);padding-right:13.75em;padding-right:var(--space-xxxxl)}.padding-x-component{padding-left:1.25em;padding-left:var(--component-padding);padding-right:1.25em;padding-right:var(--component-padding)}.padding-y-md{padding-top:1.25em;padding-top:var(--space-md);padding-bottom:1.25em;padding-bottom:var(--space-md)}.padding-y-xxxxs{padding-top:0.125em;padding-top:var(--space-xxxxs);padding-bottom:0.125em;padding-bottom:var(--space-xxxxs)}.padding-y-xxxs{padding-top:0.25em;padding-top:var(--space-xxxs);padding-bottom:0.25em;padding-bottom:var(--space-xxxs)}.padding-y-xxs{padding-top:0.375em;padding-top:var(--space-xxs);padding-bottom:0.375em;padding-bottom:var(--space-xxs)}.padding-y-xs{padding-top:0.5em;padding-top:var(--space-xs);padding-bottom:0.5em;padding-bottom:var(--space-xs)}.padding-y-sm{padding-top:0.75em;padding-top:var(--space-sm);padding-bottom:0.75em;padding-bottom:var(--space-sm)}.padding-y-lg{padding-top:2em;padding-top:var(--space-lg);padding-bottom:2em;padding-bottom:var(--space-lg)}.padding-y-xl{padding-top:3.25em;padding-top:var(--space-xl);padding-bottom:3.25em;padding-bottom:var(--space-xl)}.padding-y-xxl{padding-top:5.25em;padding-top:var(--space-xxl);padding-bottom:5.25em;padding-bottom:var(--space-xxl)}.padding-y-xxxl{padding-top:8.5em;padding-top:var(--space-xxxl);padding-bottom:8.5em;padding-bottom:var(--space-xxxl)}.padding-y-xxxxl{padding-top:13.75em;padding-top:var(--space-xxxxl);padding-bottom:13.75em;padding-bottom:var(--space-xxxxl)}.padding-y-component{padding-top:1.25em;padding-top:var(--component-padding);padding-bottom:1.25em;padding-bottom:var(--component-padding)}@media not all and (min-width: 32rem){.has-padding\@xs{padding:0 !important}}@media not all and (min-width: 48rem){.has-padding\@sm{padding:0 !important}}@media not all and (min-width: 64rem){.has-padding\@md{padding:0 !important}}@media not all and (min-width: 80rem){.has-padding\@lg{padding:0 !important}}@media not all and (min-width: 90rem){.has-padding\@xl{padding:0 !important}}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-replace{overflow:hidden;color:transparent;text-indent:100%;white-space:nowrap}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}@media (min-width: 32rem){.text-center\@xs{text-align:center}.text-left\@xs{text-align:left}.text-right\@xs{text-align:right}}@media (min-width: 48rem){.text-center\@sm{text-align:center}.text-left\@sm{text-align:left}.text-right\@sm{text-align:right}}@media (min-width: 64rem){.text-center\@md{text-align:center}.text-left\@md{text-align:left}.text-right\@md{text-align:right}}@media (min-width: 80rem){.text-center\@lg{text-align:center}.text-left\@lg{text-align:left}.text-right\@lg{text-align:right}}@media (min-width: 90rem){.text-center\@xl{text-align:center}.text-left\@xl{text-align:left}.text-right\@xl{text-align:right}}.color-inherit{color:inherit}.color-contrast-medium{color:hsl(240, 1%, 48%);color:var(--color-contrast-medium, #79797c)}.color-contrast-high{color:hsl(240, 4%, 20%);color:var(--color-contrast-high, #313135)}.color-contrast-higher{color:hsl(240, 8%, 12%);color:var(--color-contrast-higher, #1c1c21)}.color-primary{color:hsl(220, 90%, 56%);color:var(--color-primary, #2a6df4)}.color-accent{color:hsl(355, 90%, 61%);color:var(--color-accent, #f54251)}.color-success{color:hsl(94, 48%, 56%);color:var(--color-success, #88c559)}.color-warning{color:hsl(46, 100%, 61%);color:var(--color-warning, #ffd138)}.color-error{color:hsl(355, 90%, 61%);color:var(--color-error, #f54251)}.width-100\%{width:100%}.height-100\%{height:100%}.media-wrapper{position:relative;height:0;padding-bottom:56.25%}.media-wrapper iframe,.media-wrapper video,.media-wrapper img{position:absolute;top:0;left:0;width:100%;height:100%}.media-wrapper video,.media-wrapper img{-o-object-fit:cover;object-fit:cover}.media-wrapper--4\:3{padding-bottom:75%}:root,[data-theme="default"]{--color-primary-darker:hsl(220, 90%, 36%);--color-primary-darker-h:220;--color-primary-darker-s:90%;--color-primary-darker-l:36%;--color-primary-dark:hsl(220, 90%, 46%);--color-primary-dark-h:220;--color-primary-dark-s:90%;--color-primary-dark-l:46%;--color-primary:hsl(220, 90%, 56%);--color-primary-h:220;--color-primary-s:90%;--color-primary-l:56%;--color-primary-light:hsl(220, 90%, 66%);--color-primary-light-h:220;--color-primary-light-s:90%;--color-primary-light-l:66%;--color-primary-lighter:hsl(220, 90%, 76%);--color-primary-lighter-h:220;--color-primary-lighter-s:90%;--color-primary-lighter-l:76%;--color-accent-darker:hsl(355, 90%, 41%);--color-accent-darker-h:355;--color-accent-darker-s:90%;--color-accent-darker-l:41%;--color-accent-dark:hsl(355, 90%, 51%);--color-accent-dark-h:355;--color-accent-dark-s:90%;--color-accent-dark-l:51%;--color-accent:hsl(355, 90%, 61%);--color-accent-h:355;--color-accent-s:90%;--color-accent-l:61%;--color-accent-light:hsl(355, 90%, 71%);--color-accent-light-h:355;--color-accent-light-s:90%;--color-accent-light-l:71%;--color-accent-lighter:hsl(355, 90%, 81%);--color-accent-lighter-h:355;--color-accent-lighter-s:90%;--color-accent-lighter-l:81%;--color-black:hsl(240, 8%, 12%);--color-black-h:240;--color-black-s:8%;--color-black-l:12%;--color-white:hsl(0, 0%, 100%);--color-white-h:0;--color-white-s:0%;--color-white-l:100%;--color-success-darker:hsl(94, 48%, 36%);--color-success-darker-h:94;--color-success-darker-s:48%;--color-success-darker-l:36%;--color-success-dark:hsl(94, 48%, 46%);--color-success-dark-h:94;--color-success-dark-s:48%;--color-success-dark-l:46%;--color-success:hsl(94, 48%, 56%);--color-success-h:94;--color-success-s:48%;--color-success-l:56%;--color-success-light:hsl(94, 48%, 66%);--color-success-light-h:94;--color-success-light-s:48%;--color-success-light-l:66%;--color-success-lighter:hsl(94, 48%, 76%);--color-success-lighter-h:94;--color-success-lighter-s:48%;--color-success-lighter-l:76%;--color-error-darker:hsl(355, 90%, 41%);--color-error-darker-h:355;--color-error-darker-s:90%;--color-error-darker-l:41%;--color-error-dark:hsl(355, 90%, 51%);--color-error-dark-h:355;--color-error-dark-s:90%;--color-error-dark-l:51%;--color-error:hsl(355, 90%, 61%);--color-error-h:355;--color-error-s:90%;--color-error-l:61%;--color-error-light:hsl(355, 90%, 71%);--color-error-light-h:355;--color-error-light-s:90%;--color-error-light-l:71%;--color-error-lighter:hsl(355, 90%, 81%);--color-error-lighter-h:355;--color-error-lighter-s:90%;--color-error-lighter-l:81%;--color-warning-darker:hsl(46, 100%, 41%);--color-warning-darker-h:46;--color-warning-darker-s:100%;--color-warning-darker-l:41%;--color-warning-dark:hsl(46, 100%, 51%);--color-warning-dark-h:46;--color-warning-dark-s:100%;--color-warning-dark-l:51%;--color-warning:hsl(46, 100%, 61%);--color-warning-h:46;--color-warning-s:100%;--color-warning-l:61%;--color-warning-light:hsl(46, 100%, 71%);--color-warning-light-h:46;--color-warning-light-s:100%;--color-warning-light-l:71%;--color-warning-lighter:hsl(46, 100%, 81%);--color-warning-lighter-h:46;--color-warning-lighter-s:100%;--color-warning-lighter-l:81%;--color-bg:hsl(0, 0%, 100%);--color-bg-h:0;--color-bg-s:0%;--color-bg-l:100%;--color-contrast-lower:hsl(0, 0%, 95%);--color-contrast-lower-h:0;--color-contrast-lower-s:0%;--color-contrast-lower-l:95%;--color-contrast-low:hsl(240, 1%, 83%);--color-contrast-low-h:240;--color-contrast-low-s:1%;--color-contrast-low-l:83%;--color-contrast-medium:hsl(240, 1%, 48%);--color-contrast-medium-h:240;--color-contrast-medium-s:1%;--color-contrast-medium-l:48%;--color-contrast-high:hsl(240, 4%, 20%);--color-contrast-high-h:240;--color-contrast-high-s:4%;--color-contrast-high-l:20%;--color-contrast-higher:hsl(240, 8%, 12%);--color-contrast-higher-h:240;--color-contrast-higher-s:8%;--color-contrast-higher-l:12%}@supports (--css: variables){@media (min-width: 64rem){:root{--space-unit: 1.25em}}}:root{--radius: 0.25em}:root{--font-primary: sans-serif;--text-base-size: 1em;--text-scale-ratio: 1.2;--text-xs: calc(1em/var(--text-scale-ratio)/var(--text-scale-ratio));--text-sm: calc(var(--text-xs)*var(--text-scale-ratio));--text-md: calc(var(--text-sm)*var(--text-scale-ratio)*var(--text-scale-ratio));--text-lg: calc(var(--text-md)*var(--text-scale-ratio));--text-xl: calc(var(--text-lg)*var(--text-scale-ratio));--text-xxl: calc(var(--text-xl)*var(--text-scale-ratio));--text-xxxl: calc(var(--text-xxl)*var(--text-scale-ratio));--body-line-height: 1.4;--heading-line-height: 1.2;--font-primary-capital-letter: 1}@supports (--css: variables){@media (min-width: 64rem){:root{--text-base-size: 1.25em;--text-scale-ratio: 1.25}}}mark{background-color:hsla(355, 90%, 61%, 0.2);background-color:hsla(var(--color-accent-h), var(--color-accent-s), var(--color-accent-l), 0.2);color:inherit}.text-component{--line-height-multiplier: 1;--text-vspace-multiplier: 1}.text-component blockquote{padding-left:1em;border-left:4px solid hsl(240, 1%, 83%);border-left:4px solid var(--color-contrast-low)}.text-component hr{background:hsl(240, 1%, 83%);background:var(--color-contrast-low);height:1px}.text-component figcaption{font-size:0.83333em;font-size:var(--text-sm);color:hsl(240, 1%, 48%);color:var(--color-contrast-medium)}.article.text-component{--line-height-multiplier: 1.13;--text-vspace-multiplier: 1.2}:root{--btn-font-size: 1em;--btn-font-size-sm: calc(var(--btn-font-size) - 0.2em);--btn-font-size-md: calc(var(--btn-font-size) + 0.2em);--btn-font-size-lg: calc(var(--btn-font-size) + 0.4em);--btn-radius: 0.25em;--btn-padding-x: var(--space-sm);--btn-padding-y: var(--space-xs)}.btn{--color-shadow: hsla(240, 8%, 12%, 0.15);--color-shadow: hsla(var(--color-black-h), var(--color-black-s), var(--color-black-l), 0.15);box-shadow:0 4px 16px hsla(240, 8%, 12%, 0.15);box-shadow:0 4px 16px hsla(var(--color-black-h), var(--color-black-s), var(--color-black-l), 0.15);cursor:pointer}.btn--primary{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.btn--accent{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.btn--disabled{opacity:0.6}:root{--form-control-padding-x: var(--space-sm);--form-control-padding-y: var(--space-xs);--form-control-radius: 0.25em}.form-control{border:2px solid hsl(240, 1%, 83%);border:2px solid var(--color-contrast-low)}.form-control:focus{outline:none;border-color:hsl(220, 90%, 56%);border-color:var(--color-primary);--color-shadow: hsla(220, 90%, 56%, 0.2);--color-shadow: hsla(var(--color-primary-h), var(--color-primary-s), var(--color-primary-l), 0.2);box-shadow:undefined;box-shadow:0 0 0 3px var(--color-shadow)}.form-control:focus:focus{box-shadow:0 0 0 3px hsla(220, 90%, 56%, 0.2);box-shadow:0 0 0 3px var(--color-shadow)}.form-control[aria-invalid="true"]{border-color:hsl(355, 90%, 61%);border-color:var(--color-error)}.form-control[aria-invalid="true"]:focus{--color-shadow: hsla(355, 90%, 61%, 0.2);--color-shadow: hsla(var(--color-error-h), var(--color-error-s), var(--color-error-l), 0.2);box-shadow:undefined;box-shadow:0 0 0 3px var(--color-shadow)}.form-control[aria-invalid="true"]:focus:focus{box-shadow:0 0 0 3px hsla(355, 90%, 61%, 0.2);box-shadow:0 0 0 3px var(--color-shadow)}.form-label{font-size:0.83333em;font-size:var(--text-sm)}:root{--cd-color-1:hsl(206, 21%, 24%);--cd-color-1-h:206;--cd-color-1-s:21%;--cd-color-1-l:24%;--cd-color-2:hsl(205, 38%, 89%);--cd-color-2-h:205;--cd-color-2-s:38%;--cd-color-2-l:89%;--cd-color-3:hsl(207, 10%, 55%);--cd-color-3-h:207;--cd-color-3-s:10%;--cd-color-3-l:55%;--cd-color-4:hsl(111, 51%, 60%);--cd-color-4-h:111;--cd-color-4-s:51%;--cd-color-4-l:60%;--cd-color-5:hsl(356, 53%, 49%);--cd-color-5-h:356;--cd-color-5-s:53%;--cd-color-5-l:49%;--cd-color-6:hsl(47, 85%, 61%);--cd-color-6-h:47;--cd-color-6-s:85%;--cd-color-6-l:61%;--cd-header-height: 200px;--font-primary: 'Droid Serif', serif;--font-secondary: 'Open Sans', sans-serif}@supports (--css: variables){@media (min-width: 64rem){:root{--cd-header-height: 300px}}}.cd-main-header{height:200px;height:var(--cd-header-height);background:hsl(206, 21%, 24%);background:var(--cd-color-1);color:hsl(0, 0%, 100%);color:var(--color-white);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.cd-main-header h1{font-family:'Open Sans', sans-serif;font-family:var(--font-secondary);color:inherit}.cd-timeline{overflow:hidden;padding:2em 0;padding:var(--space-lg) 0;color:hsl(207, 10%, 55%);color:var(--cd-color-3);background-color:hsl(205, 38%, 93.45%);background-color:hsl(var(--cd-color-2-h), var(--cd-color-2-s), calc(var(--cd-color-2-l)*1.05));font-family:'Droid Serif', serif;font-family:var(--font-primary)}.cd-timeline h2{font-family:'Open Sans', sans-serif;font-family:var(--font-secondary);font-weight:700}.cd-timeline__container{position:relative;padding:1.25em 0;padding:var(--space-md) 0}.cd-timeline__container::before{content:'';position:absolute;top:0;left:18px;height:100%;width:4px;background:hsl(205, 38%, 89%);background:var(--cd-color-2)}@media (min-width: 64rem){.cd-timeline__container::before{left:50%;-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%)}}.cd-timeline__block{display:-ms-flexbox;display:flex;position:relative;z-index:1;margin-bottom:2em;margin-bottom:var(--space-lg)}.cd-timeline__block:last-child{margin-bottom:0}@media (min-width: 64rem){.cd-timeline__block:nth-child(even){-ms-flex-direction:row-reverse;flex-direction:row-reverse}}.cd-timeline__img{display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center;-ms-flex-negative:0;flex-shrink:0;width:40px;height:40px;border-radius:50%;box-shadow:0 0 0 4px hsl(0, 0%, 100%),inset 0 2px 0 rgba(0,0,0,0.08),0 3px 0 4px rgba(0,0,0,0.05);box-shadow:0 0 0 4px var(--color-white),inset 0 2px 0 rgba(0,0,0,0.08),0 3px 0 4px rgba(0,0,0,0.05)}.cd-timeline__img img{width:24px;height:24px}@media (min-width: 64rem){.cd-timeline__img{width:60px;height:60px;-ms-flex-order:1;order:1;margin-left:calc(5% - 30px);will-change:transform}.cd-timeline__block:nth-child(even) .cd-timeline__img{margin-right:calc(5% - 30px)}}.cd-timeline__img--picture{background-color:hsl(111, 51%, 60%);background-color:var(--cd-color-4)}.cd-timeline__img--movie{background-color:hsl(356, 53%, 49%);background-color:var(--cd-color-5)}.cd-timeline__img--location{background-color:hsl(47, 85%, 61%);background-color:var(--cd-color-6)}.cd-timeline__content{-ms-flex-positive:1;flex-grow:1;position:relative;margin-left:1.25em;margin-left:var(--space-md);background:hsl(0, 0%, 100%);background:var(--color-white);border-radius:0.25em;border-radius:var(--radius-md);padding:1.25em;padding:var(--space-md);box-shadow:0 3px 0 hsl(205, 38%, 89%);box-shadow:0 3px 0 var(--cd-color-2)}.cd-timeline__content::before{content:'';position:absolute;top:16px;right:100%;width:0;height:0;border:7px solid transparent;border-right-color:hsl(0, 0%, 100%);border-right-color:var(--color-white)}.cd-timeline__content h2{color:hsl(206, 21%, 24%);color:var(--cd-color-1)}@media (min-width: 64rem){.cd-timeline__content{width:45%;-ms-flex-positive:0;flex-grow:0;will-change:transform;margin:0;font-size:0.8em;--line-height-multiplier: 1.2}.cd-timeline__content::before{top:24px}.cd-timeline__block:nth-child(odd) .cd-timeline__content::before{right:auto;left:100%;width:0;height:0;border:7px solid transparent;border-left-color:hsl(0, 0%, 100%);border-left-color:var(--color-white)}}.cd-timeline__date{color:hsla(207, 10%, 55%, 0.7);color:hsla(var(--cd-color-3-h), var(--cd-color-3-s), var(--cd-color-3-l), 0.7)}@media (min-width: 64rem){.cd-timeline__date{position:absolute;width:100%;left:120%;top:20px}.cd-timeline__block:nth-child(even) .cd-timeline__date{left:auto;right:120%;text-align:right}}@media (min-width: 64rem){.cd-timeline__img--hidden,.cd-timeline__content--hidden{visibility:hidden}.cd-timeline__img--bounce-in{-webkit-animation:cd-bounce-1 0.6s;animation:cd-bounce-1 0.6s}.cd-timeline__content--bounce-in{-webkit-animation:cd-bounce-2 0.6s;animation:cd-bounce-2 0.6s}.cd-timeline__block:nth-child(even) .cd-timeline__content--bounce-in{-webkit-animation-name:cd-bounce-2-inverse;animation-name:cd-bounce-2-inverse}}@-webkit-keyframes cd-bounce-1{0%{opacity:0;-webkit-transform:scale(0.5);transform:scale(0.5)}60%{opacity:1;-webkit-transform:scale(1.2);transform:scale(1.2)}100%{-webkit-transform:scale(1);transform:scale(1)}}@keyframes cd-bounce-1{0%{opacity:0;-webkit-transform:scale(0.5);transform:scale(0.5)}60%{opacity:1;-webkit-transform:scale(1.2);transform:scale(1.2)}100%{-webkit-transform:scale(1);transform:scale(1)}}@-webkit-keyframes cd-bounce-2{0%{opacity:0;-webkit-transform:translateX(-100px);transform:translateX(-100px)}60%{opacity:1;-webkit-transform:translateX(20px);transform:translateX(20px)}100%{-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes cd-bounce-2{0%{opacity:0;-webkit-transform:translateX(-100px);transform:translateX(-100px)}60%{opacity:1;-webkit-transform:translateX(20px);transform:translateX(20px)}100%{-webkit-transform:translateX(0);transform:translateX(0)}}@-webkit-keyframes cd-bounce-2-inverse{0%{opacity:0;-webkit-transform:translateX(100px);transform:translateX(100px)}60%{opacity:1;-webkit-transform:translateX(-20px);transform:translateX(-20px)}100%{-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes cd-bounce-2-inverse{0%{opacity:0;-webkit-transform:translateX(100px);transform:translateX(100px)}60%{opacity:1;-webkit-transform:translateX(-20px);transform:translateX(-20px)}100%{-webkit-transform:translateX(0);transform:translateX(0)}} diff --git a/pydis_site/static/images/timeline/cd-icon-location.svg b/pydis_site/static/images/timeline/cd-icon-location.svg new file mode 100755 index 00000000..6128fecd --- /dev/null +++ b/pydis_site/static/images/timeline/cd-icon-location.svg @@ -0,0 +1,4 @@ + + + diff --git a/pydis_site/static/images/timeline/cd-icon-movie.svg b/pydis_site/static/images/timeline/cd-icon-movie.svg new file mode 100755 index 00000000..498a93fa --- /dev/null +++ b/pydis_site/static/images/timeline/cd-icon-movie.svg @@ -0,0 +1,4 @@ + + + diff --git a/pydis_site/static/images/timeline/cd-icon-picture.svg b/pydis_site/static/images/timeline/cd-icon-picture.svg new file mode 100755 index 00000000..dd5f180b --- /dev/null +++ b/pydis_site/static/images/timeline/cd-icon-picture.svg @@ -0,0 +1,5 @@ + + + diff --git a/pydis_site/static/js/timeline/main.js b/pydis_site/static/js/timeline/main.js new file mode 100644 index 00000000..a4bf4f31 --- /dev/null +++ b/pydis_site/static/js/timeline/main.js @@ -0,0 +1,71 @@ +(function(){ + // Vertical Timeline - by CodyHouse.co + function VerticalTimeline( element ) { + this.element = element; + this.blocks = this.element.getElementsByClassName("cd-timeline__block"); + this.images = this.element.getElementsByClassName("cd-timeline__img"); + this.contents = this.element.getElementsByClassName("cd-timeline__content"); + this.offset = 0.8; + this.hideBlocks(); + }; + + VerticalTimeline.prototype.hideBlocks = function() { + if ( !"classList" in document.documentElement ) { + return; // no animation on older browsers + } + //hide timeline blocks which are outside the viewport + var self = this; + for( var i = 0; i < this.blocks.length; i++) { + (function(i){ + if( self.blocks[i].getBoundingClientRect().top > window.innerHeight*self.offset ) { + self.images[i].classList.add("cd-timeline__img--hidden"); + self.contents[i].classList.add("cd-timeline__content--hidden"); + } + })(i); + } + }; + + VerticalTimeline.prototype.showBlocks = function() { + if ( ! "classList" in document.documentElement ) { + return; + } + var self = this; + for( var i = 0; i < this.blocks.length; i++) { + (function(i){ + if( self.contents[i].classList.contains("cd-timeline__content--hidden") && self.blocks[i].getBoundingClientRect().top <= window.innerHeight*self.offset ) { + // add bounce-in animation + self.images[i].classList.add("cd-timeline__img--bounce-in"); + self.contents[i].classList.add("cd-timeline__content--bounce-in"); + self.images[i].classList.remove("cd-timeline__img--hidden"); + self.contents[i].classList.remove("cd-timeline__content--hidden"); + } + })(i); + } + }; + + var verticalTimelines = document.getElementsByClassName("js-cd-timeline"), + verticalTimelinesArray = [], + scrolling = false; + if( verticalTimelines.length > 0 ) { + for( var i = 0; i < verticalTimelines.length; i++) { + (function(i){ + verticalTimelinesArray.push(new VerticalTimeline(verticalTimelines[i])); + })(i); + } + + //show timeline blocks on scrolling + window.addEventListener("scroll", function(event) { + if( !scrolling ) { + scrolling = true; + (!window.requestAnimationFrame) ? setTimeout(checkTimelineScroll, 250) : window.requestAnimationFrame(checkTimelineScroll); + } + }); + } + + function checkTimelineScroll() { + verticalTimelinesArray.forEach(function(timeline){ + timeline.showBlocks(); + }); + scrolling = false; + }; +})(); -- cgit v1.2.3 From 49e7243f6527140dbdb29a2f1dd994839f248dba Mon Sep 17 00:00:00 2001 From: Eivind Teig Date: Fri, 11 Sep 2020 23:43:06 +0200 Subject: Allow blank/null input to the nomination reason. We are lowering the threshold for nomination. By allowing the users to make a nomination without a reason might make this feature more attractive amongst members of staff. --- .../0063_Allow_blank_or_null_for_nomination_reason.py | 18 ++++++++++++++++++ pydis_site/apps/api/models/bot/nomination.py | 4 +++- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 pydis_site/apps/api/migrations/0063_Allow_blank_or_null_for_nomination_reason.py diff --git a/pydis_site/apps/api/migrations/0063_Allow_blank_or_null_for_nomination_reason.py b/pydis_site/apps/api/migrations/0063_Allow_blank_or_null_for_nomination_reason.py new file mode 100644 index 00000000..9eb05eaa --- /dev/null +++ b/pydis_site/apps/api/migrations/0063_Allow_blank_or_null_for_nomination_reason.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.9 on 2020-09-11 21:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0062_merge_20200901_1459'), + ] + + operations = [ + migrations.AlterField( + model_name='nomination', + name='reason', + field=models.TextField(blank=True, help_text='Why this user was nominated.', null=True), + ), + ] diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py index 21e34e87..183b22d5 100644 --- a/pydis_site/apps/api/models/bot/nomination.py +++ b/pydis_site/apps/api/models/bot/nomination.py @@ -18,7 +18,9 @@ class Nomination(ModelReprMixin, models.Model): related_name='nomination_set' ) reason = models.TextField( - help_text="Why this user was nominated." + help_text="Why this user was nominated.", + null=True, + blank=True ) user = models.ForeignKey( User, -- cgit v1.2.3 From 1ed224135e48d5d1840d14d446575b8cbc5a7a6e Mon Sep 17 00:00:00 2001 From: Eivind Teig Date: Sat, 12 Sep 2020 00:13:32 +0200 Subject: Fix a broken test for nomination reason. We no longer return 400 if a reason is missing. --- pydis_site/apps/api/tests/test_nominations.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pydis_site/apps/api/tests/test_nominations.py b/pydis_site/apps/api/tests/test_nominations.py index 92c62c87..b37135f8 100644 --- a/pydis_site/apps/api/tests/test_nominations.py +++ b/pydis_site/apps/api/tests/test_nominations.py @@ -80,7 +80,7 @@ class CreationTests(APISubdomainTestCase): 'actor': ['This field is required.'] }) - def test_returns_400_for_missing_reason(self): + def test_returns_201_for_missing_reason(self): url = reverse('bot:nomination-list', host='api') data = { 'user': self.user.id, @@ -88,10 +88,7 @@ class CreationTests(APISubdomainTestCase): } response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'reason': ['This field is required.'] - }) + self.assertEqual(response.status_code, 201) def test_returns_400_for_bad_user(self): url = reverse('bot:nomination-list', host='api') -- cgit v1.2.3 From 2029cf511ee61d48eca94811e6e891a6c827de7e Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 18 Sep 2020 04:30:05 +1000 Subject: Remove TagAdmin model as Tags were removed from site. --- pydis_site/apps/api/admin.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index 271ff119..4bf2a6ee 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -242,29 +242,6 @@ class RoleAdmin(admin.ModelAdmin): permissions_with_calc_link.short_description = "Permissions" -class TagAdmin(admin.ModelAdmin): - """Admin formatting for the Tag model.""" - - fields = ("title", "embed", "preview") - readonly_fields = ("preview",) - search_fields = ("title", "embed") - - @staticmethod - def preview(instance: Tag) -> Optional[str]: - """Render tag markdown contents to preview actual appearance.""" - if instance.embed: - import markdown - return format_html( - markdown.markdown( - instance.embed["description"], - extensions=[ - "markdown.extensions.nl2br", - "markdown.extensions.extra" - ] - ) - ) - - class StaffRolesFilter(admin.SimpleListFilter): """Filter options for Staff Roles.""" -- cgit v1.2.3 From 4ad759c21ca91bc38727c27083e8aa48b30b7325 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 18 Sep 2020 11:46:53 +1000 Subject: Use admin.register decorators. --- pydis_site/apps/api/admin.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index 4bf2a6ee..6116fbf8 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -22,6 +22,7 @@ from .models import ( ) +@admin.register(LogEntry) class LogEntryAdmin(admin.ModelAdmin): """Allows viewing logs in the Django Admin without allowing edits.""" @@ -53,6 +54,7 @@ class LogEntryAdmin(admin.ModelAdmin): return False +@admin.register(DeletedMessage) class DeletedMessageAdmin(admin.ModelAdmin): """Admin formatting for the DeletedMessage model.""" @@ -109,6 +111,7 @@ class DeletedMessageAdmin(admin.ModelAdmin): ) +@admin.register(MessageDeletionContext) class MessageDeletionContextAdmin(admin.ModelAdmin): """Admin formatting for the MessageDeletionContext model.""" @@ -123,6 +126,7 @@ class MessageDeletionContextAdmin(admin.ModelAdmin): ) +@admin.register(Infraction) class InfractionAdmin(admin.ModelAdmin): """Admin formatting for the Infraction model.""" @@ -167,6 +171,7 @@ class InfractionAdmin(admin.ModelAdmin): ) +@admin.register(Nomination) class NominationAdmin(admin.ModelAdmin): """Admin formatting for the Nomination model.""" @@ -204,12 +209,14 @@ class NominationAdmin(admin.ModelAdmin): list_filter = ("active",) +@admin.register(OffTopicChannelName) class OffTopicChannelNameAdmin(admin.ModelAdmin): """Admin formatting for the OffTopicChannelName model.""" search_fields = ("name",) +@admin.register(Role) class RoleAdmin(admin.ModelAdmin): """Admin formatting for the Role model.""" @@ -266,6 +273,7 @@ class StaffRolesFilter(admin.SimpleListFilter): return queryset.filter(roles__name=value) +@admin.register(User) class UserAdmin(admin.ModelAdmin): """Admin formatting for the User model.""" @@ -283,13 +291,5 @@ class UserAdmin(admin.ModelAdmin): admin.site.register(BotSetting) -admin.site.register(DeletedMessage, DeletedMessageAdmin) admin.site.register(DocumentationLink) -admin.site.register(Infraction, InfractionAdmin) -admin.site.register(LogEntry, LogEntryAdmin) -admin.site.register(MessageDeletionContext, MessageDeletionContextAdmin) -admin.site.register(Nomination, NominationAdmin) admin.site.register(OffensiveMessage) -admin.site.register(OffTopicChannelName, OffTopicChannelNameAdmin) -admin.site.register(Role, RoleAdmin) -admin.site.register(User, UserAdmin) -- cgit v1.2.3 From a311ad08003203a5aa38bf49fec69d6d42f74439 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 18 Sep 2020 11:48:54 +1000 Subject: Update UserAdmin to use new role values, pretty colours. --- pydis_site/apps/api/admin.py | 78 ++++++++++++++++++++-------------- pydis_site/apps/api/models/bot/user.py | 4 ++ 2 files changed, 50 insertions(+), 32 deletions(-) diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index 6116fbf8..6101f88c 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -1,9 +1,10 @@ +from __future__ import annotations + import json -from typing import Optional, Tuple +from typing import Optional from django import urls from django.contrib import admin -from django.db.models.query import QuerySet from django.http import HttpRequest from django.utils.html import format_html @@ -249,45 +250,58 @@ class RoleAdmin(admin.ModelAdmin): permissions_with_calc_link.short_description = "Permissions" -class StaffRolesFilter(admin.SimpleListFilter): - """Filter options for Staff Roles.""" +class UserTopRoleFilter(admin.SimpleListFilter): + """List Filter for User list Admin page.""" - title = "Staff Role" - parameter_name = "staff_role" + title = "Role" + parameter_name = "role" - @staticmethod - def lookups(*_) -> Tuple[Tuple[str, str], ...]: - """Available filter options.""" - return ( - ("Owners", "Owners"), - ("Admins", "Admins"), - ("Moderators", "Moderators"), - ("Core Developers", "Core Developers"), - ("Helpers", "Helpers"), - ) + def lookups(self, request, model_admin: UserAdmin): + """Selectable values for viewer to filter by.""" + roles = Role.objects.all() + return ((r.name, r.name) for r in roles) - def queryset(self, request: HttpRequest, queryset: QuerySet) -> Optional[QuerySet]: - """Returned data filter based on selected option.""" - value = self.value() - if value: - return queryset.filter(roles__name=value) + def queryset(self, request, queryset): + if not self.value(): + return + role = Role.objects.get(name=self.value()) + return queryset.filter(roles__contains=[role.id]) @admin.register(User) class UserAdmin(admin.ModelAdmin): """Admin formatting for the User model.""" - search_fields = ("name", "id", "roles__name", "roles__id") - list_filter = ("in_guild", StaffRolesFilter) - exclude = ("name", "discriminator") - readonly_fields = ( - "__str__", - "id", - "avatar_hash", - "top_role", - "roles", - "in_guild", - ) + def top_role_coloured(self, obj: User): + """Returns the top role of the user with html style matching role colour.""" + return format_html( + f'{obj.top_role.name}' + ) + + top_role_coloured.short_description = "Top Role" + + def all_roles_coloured(self, obj: User): + """Returns all user roles with html style matching role colours.""" + roles = Role.objects.filter(id__in=obj.roles) + return format_html( + "
".join( + f'{r.name}' for r in roles + ) + ) + + all_roles_coloured.short_description = "All Roles" + + search_fields = ("name", "id", "roles") + list_filter = (UserTopRoleFilter, "in_guild") + list_display = ("username", "top_role_coloured", "in_guild") + fields = ("username", "id", "in_guild", "all_roles_coloured") + sortable_by = ("username",) + + def has_add_permission(self, request): + return False + + def has_change_permission(self, request, obj=None): + return False admin.site.register(BotSetting) diff --git a/pydis_site/apps/api/models/bot/user.py b/pydis_site/apps/api/models/bot/user.py index cd2d58b9..70a30449 100644 --- a/pydis_site/apps/api/models/bot/user.py +++ b/pydis_site/apps/api/models/bot/user.py @@ -75,3 +75,7 @@ class User(ModelReprMixin, models.Model): if not roles: return Role.objects.get(name="Developers") return max(roles) + + @property + def username(self): + return f"{self.name}#{self.discriminator:04d}" -- cgit v1.2.3 From dfbce19d39b0b6243865e682b8e6d86b8a028a71 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 18 Sep 2020 12:00:13 +1000 Subject: Add verbose names for user fields that need capitalisation fixes. --- pydis_site/apps/api/models/bot/user.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pydis_site/apps/api/models/bot/user.py b/pydis_site/apps/api/models/bot/user.py index 70a30449..a8604001 100644 --- a/pydis_site/apps/api/models/bot/user.py +++ b/pydis_site/apps/api/models/bot/user.py @@ -26,11 +26,12 @@ class User(ModelReprMixin, models.Model): message="User IDs cannot be negative." ), ), + verbose_name="ID", help_text="The ID of this user, taken from Discord." ) name = models.CharField( max_length=32, - help_text="The username, taken from Discord." + help_text="The username, taken from Discord.", ) discriminator = models.PositiveSmallIntegerField( validators=( @@ -57,7 +58,8 @@ class User(ModelReprMixin, models.Model): ) in_guild = models.BooleanField( default=True, - help_text="Whether this user is in our server." + help_text="Whether this user is in our server.", + verbose_name="In Guild" ) def __str__(self): -- cgit v1.2.3 From 77c9e3ffce7992706202cf20ae3addf42c4dbf6c Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 18 Sep 2020 12:40:24 +1000 Subject: Add return types and docstrings for new user admin changes. --- pydis_site/apps/api/admin.py | 29 ++++++++++++++++++----------- pydis_site/apps/api/models/bot/user.py | 11 ++++++++--- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index 6101f88c..d77ae620 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -1,12 +1,13 @@ from __future__ import annotations import json -from typing import Optional +from typing import Iterable, Optional, Tuple from django import urls from django.contrib import admin +from django.db.models import QuerySet from django.http import HttpRequest -from django.utils.html import format_html +from django.utils.html import SafeString, format_html from .models import ( BotSetting, @@ -256,12 +257,13 @@ class UserTopRoleFilter(admin.SimpleListFilter): title = "Role" parameter_name = "role" - def lookups(self, request, model_admin: UserAdmin): + def lookups(self, request: HttpRequest, model_admin: UserAdmin) -> Iterable[Tuple[str, str]]: """Selectable values for viewer to filter by.""" roles = Role.objects.all() return ((r.name, r.name) for r in roles) - def queryset(self, request, queryset): + def queryset(self, request: HttpRequest, queryset: QuerySet) -> Optional[QuerySet]: + """Query to filter the list of Users against.""" if not self.value(): return role = Role.objects.get(name=self.value()) @@ -272,20 +274,23 @@ class UserTopRoleFilter(admin.SimpleListFilter): class UserAdmin(admin.ModelAdmin): """Admin formatting for the User model.""" - def top_role_coloured(self, obj: User): + def top_role_coloured(self, user: User) -> SafeString: """Returns the top role of the user with html style matching role colour.""" return format_html( - f'{obj.top_role.name}' + '{1}', + user.top_role.colour, + user.top_role.name ) top_role_coloured.short_description = "Top Role" - def all_roles_coloured(self, obj: User): + def all_roles_coloured(self, user: User) -> SafeString: """Returns all user roles with html style matching role colours.""" - roles = Role.objects.filter(id__in=obj.roles) + roles = Role.objects.filter(id__in=user.roles) return format_html( "
".join( - f'{r.name}' for r in roles + f'{r.name}' + for r in roles ) ) @@ -297,10 +302,12 @@ class UserAdmin(admin.ModelAdmin): fields = ("username", "id", "in_guild", "all_roles_coloured") sortable_by = ("username",) - def has_add_permission(self, request): + def has_add_permission(self, *args) -> bool: + """Prevent adding from django admin.""" return False - def has_change_permission(self, request, obj=None): + def has_change_permission(self, *args) -> bool: + """Prevent editing from django admin.""" return False diff --git a/pydis_site/apps/api/models/bot/user.py b/pydis_site/apps/api/models/bot/user.py index a8604001..afc5ba1e 100644 --- a/pydis_site/apps/api/models/bot/user.py +++ b/pydis_site/apps/api/models/bot/user.py @@ -64,7 +64,7 @@ class User(ModelReprMixin, models.Model): def __str__(self): """Returns the name and discriminator for the current user, for display purposes.""" - return f"{self.name}#{self.discriminator:0>4}" + return f"{self.name}#{self.discriminator:04d}" @property def top_role(self) -> Role: @@ -79,5 +79,10 @@ class User(ModelReprMixin, models.Model): return max(roles) @property - def username(self): - return f"{self.name}#{self.discriminator:04d}" + def username(self) -> str: + """ + Returns the display version with name and discriminator as a standard attribute. + + For usability in read-only fields such as Django Admin. + """ + return str(self) -- cgit v1.2.3 From f132c485c7ecff661a257dcdf4b528b7300e95c1 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 18 Sep 2020 12:45:25 +1000 Subject: Fix format-style not supporting X hex casting like f-strings. --- pydis_site/apps/api/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index d77ae620..15a1ec78 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -277,8 +277,8 @@ class UserAdmin(admin.ModelAdmin): def top_role_coloured(self, user: User) -> SafeString: """Returns the top role of the user with html style matching role colour.""" return format_html( - '{1}', - user.top_role.colour, + '{1}', + f"#{user.top_role.colour:06X}", user.top_role.name ) -- cgit v1.2.3 From bbecf18704837f087e8254b657adf1c5e859dff9 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 18 Sep 2020 12:47:32 +1000 Subject: Update Role ModelAdmin to past changes, cleanup formatting. --- pydis_site/apps/api/admin.py | 45 ++++++++++++++++++++++------------ pydis_site/apps/api/models/bot/role.py | 3 ++- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index 15a1ec78..d32b4911 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -222,34 +222,47 @@ class OffTopicChannelNameAdmin(admin.ModelAdmin): class RoleAdmin(admin.ModelAdmin): """Admin formatting for the Role model.""" - exclude = ("permissions", "colour") - readonly_fields = ( - "name", - "id", - "colour_with_preview", - "permissions_with_calc_link", - "position" - ) - search_fields = ("name", "id") + def coloured_name(self, role: Role) -> SafeString: + """Role name with html style colouring.""" + return format_html( + '{1}', + f"#{role.colour:06X}", + role.name + ) + + coloured_name.short_description = "Name" - def colour_with_preview(self, instance: Role) -> str: + def colour_with_preview(self, role: Role) -> SafeString: """Show colour value in both int and hex, in bolded and coloured style.""" return format_html( - "{1} / #{0}", - f"{instance.colour:06x}", - instance.colour + "{0} ({1})", + f"#{role.colour:06x}", + role.colour ) - def permissions_with_calc_link(self, instance: Role) -> str: + colour_with_preview.short_description = "Colour" + + def permissions_with_calc_link(self, role: Role) -> SafeString: """Show permissions with link to API permissions calculator page.""" return format_html( "{0}", - instance.permissions + role.permissions ) - colour_with_preview.short_description = "Colour" permissions_with_calc_link.short_description = "Permissions" + search_fields = ("name", "id") + list_display = ("coloured_name",) + fields = ("id", "name", "colour_with_preview", "permissions_with_calc_link", "position") + + def has_add_permission(self, *args) -> bool: + """Prevent adding from django admin.""" + return False + + def has_change_permission(self, *args) -> bool: + """Prevent editing from django admin.""" + return False + class UserTopRoleFilter(admin.SimpleListFilter): """List Filter for User list Admin page.""" diff --git a/pydis_site/apps/api/models/bot/role.py b/pydis_site/apps/api/models/bot/role.py index b23fc5f4..cfadfec4 100644 --- a/pydis_site/apps/api/models/bot/role.py +++ b/pydis_site/apps/api/models/bot/role.py @@ -22,7 +22,8 @@ class Role(ModelReprMixin, models.Model): message="Role IDs cannot be negative." ), ), - help_text="The role ID, taken from Discord." + help_text="The role ID, taken from Discord.", + verbose_name="ID" ) name = models.CharField( max_length=100, -- cgit v1.2.3 From 8ed8b9ad62fd83e93d8dd5a136b7433f7240c4c2 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 18 Sep 2020 13:01:49 +1000 Subject: Add OffensiveMessage Admin model. --- pydis_site/apps/api/admin.py | 25 +++++++++++++++++++++- .../apps/api/models/bot/offensive_message.py | 9 +++++--- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index d32b4911..59cab78d 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -218,6 +218,30 @@ class OffTopicChannelNameAdmin(admin.ModelAdmin): search_fields = ("name",) +@admin.register(OffensiveMessage) +class OffensiveMessageAdmin(admin.ModelAdmin): + """Admin formatting for the OffensiveMessage model.""" + + def message_jumplink(self, message: OffensiveMessage) -> SafeString: + """Message ID hyperlinked to the direct discord jumplink.""" + return format_html( + '{1}', + message.channel_id, + message.id + ) + + message_jumplink.short_description = "Message ID" + + search_fields = ("id", "channel_id") + list_display = ("id", "channel_id", "delete_date") + fields = ("message_jumplink", "channel_id", "delete_date") + readonly_fields = ("message_jumplink", "channel_id") + + def has_add_permission(self, *args) -> bool: + """Prevent adding from django admin.""" + return False + + @admin.register(Role) class RoleAdmin(admin.ModelAdmin): """Admin formatting for the Role model.""" @@ -326,4 +350,3 @@ class UserAdmin(admin.ModelAdmin): admin.site.register(BotSetting) admin.site.register(DocumentationLink) -admin.site.register(OffensiveMessage) diff --git a/pydis_site/apps/api/models/bot/offensive_message.py b/pydis_site/apps/api/models/bot/offensive_message.py index 6c0e5ffb..74dab59b 100644 --- a/pydis_site/apps/api/models/bot/offensive_message.py +++ b/pydis_site/apps/api/models/bot/offensive_message.py @@ -24,7 +24,8 @@ class OffensiveMessage(ModelReprMixin, models.Model): limit_value=0, message="Message IDs cannot be negative." ), - ) + ), + verbose_name="Message ID" ) channel_id = models.BigIntegerField( help_text=( @@ -36,11 +37,13 @@ class OffensiveMessage(ModelReprMixin, models.Model): limit_value=0, message="Channel IDs cannot be negative." ), - ) + ), + verbose_name="Channel ID" ) delete_date = models.DateTimeField( help_text="The date on which the message will be auto-deleted.", - validators=(future_date_validator,) + validators=(future_date_validator,), + verbose_name="To Be Deleted" ) def __str__(self): -- cgit v1.2.3 From c9671fb8fe8004c79139bca9a50ccc8ed1b3dffc Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 18 Sep 2020 13:07:35 +1000 Subject: Add Used filter for OffTopicChannelName Admin model. --- pydis_site/apps/api/admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index 59cab78d..d07fc335 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -216,6 +216,7 @@ class OffTopicChannelNameAdmin(admin.ModelAdmin): """Admin formatting for the OffTopicChannelName model.""" search_fields = ("name",) + list_filter = ("used",) @admin.register(OffensiveMessage) -- cgit v1.2.3 From f4c47c976f72033f133bc90fcc8f30f291f3ad59 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 18 Sep 2020 13:40:32 +1000 Subject: Update Nomination Admin model, add actor filter. --- pydis_site/apps/api/admin.py | 47 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index d07fc335..a85b4cac 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -173,18 +173,47 @@ class InfractionAdmin(admin.ModelAdmin): ) +class NominationActorFilter(admin.SimpleListFilter): + """Actor Filter for Nomination Admin list page.""" + + title = "Actor" + parameter_name = "actor" + + def lookups(self, request: HttpRequest, model_admin: NominationAdmin) -> Iterable[Tuple[int, str]]: + """Selectable values for viewer to filter by.""" + actor_ids = Nomination.objects.order_by().values_list("actor").distinct() + actors = User.objects.filter(id__in=actor_ids) + return ((a.id, a.username) for a in actors) + + def queryset(self, request: HttpRequest, queryset: QuerySet) -> Optional[QuerySet]: + """Query to filter the list of Users against.""" + if not self.value(): + return + return queryset.filter(actor__id=self.value()) + + @admin.register(Nomination) class NominationAdmin(admin.ModelAdmin): """Admin formatting for the Nomination model.""" + search_fields = ( + "user__name", + "user__id", + "actor__name", + "actor__id", + "reason", + "end_reason" + ) + + list_filter = ("active", NominationActorFilter) + list_display = ( "user", "active", "reason", "actor", - "inserted_at", - "ended_at" ) + fields = ( "user", "active", @@ -194,6 +223,8 @@ class NominationAdmin(admin.ModelAdmin): "ended_at", "end_reason" ) + + # only allow reason fields to be edited. readonly_fields = ( "user", "active", @@ -201,14 +232,10 @@ class NominationAdmin(admin.ModelAdmin): "inserted_at", "ended_at" ) - search_fields = ( - "actor__name", - "actor__id", - "user__name", - "user__id", - "reason" - ) - list_filter = ("active",) + + def has_add_permission(self, *args) -> bool: + """Prevent adding from django admin.""" + return False @admin.register(OffTopicChannelName) -- cgit v1.2.3 From 5c2e750b6ff19248aaafbb0a1c12f8c7523b7273 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 18 Sep 2020 14:59:30 +1000 Subject: Update DeletedMessage and LogEntry Admin models, add verbose names for Message --- pydis_site/apps/api/admin.py | 167 ++++++++++++++++-------------- pydis_site/apps/api/models/bot/message.py | 6 +- 2 files changed, 95 insertions(+), 78 deletions(-) diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index a85b4cac..ca97512f 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -23,34 +23,77 @@ from .models import ( User ) +admin.site.site_header = "Python Discord | Administration" +admin.site.site_title = "Python Discord" + + +@admin.register(Infraction) +class InfractionAdmin(admin.ModelAdmin): + """Admin formatting for the Infraction model.""" + + fields = ( + "user", + "actor", + "type", + "reason", + "inserted_at", + "expires_at", + "active", + "hidden" + ) + readonly_fields = ( + "user", + "actor", + "type", + "inserted_at" + ) + list_display = ( + "type", + "user", + "actor", + "inserted_at", + "expires_at", + "reason", + "active", + ) + search_fields = ( + "id", + "user__name", + "user__id", + "actor__name", + "actor__id", + "reason", + "type" + ) + list_filter = ( + "type", + "hidden", + "active" + ) + @admin.register(LogEntry) class LogEntryAdmin(admin.ModelAdmin): """Allows viewing logs in the Django Admin without allowing edits.""" actions = None - list_display = ('timestamp', 'application', 'level', 'message') + list_display = ('timestamp', 'level', 'message') fieldsets = ( ('Overview', {'fields': ('timestamp', 'application', 'logger_name')}), ('Metadata', {'fields': ('level', 'module', 'line')}), ('Contents', {'fields': ('message',)}) ) - list_filter = ('application', 'level', 'timestamp') + list_filter = ('level', 'timestamp') search_fields = ('message',) - readonly_fields = ( - 'application', - 'logger_name', - 'timestamp', - 'level', - 'module', - 'line', - 'message' - ) def has_add_permission(self, request: HttpRequest) -> bool: """Deny manual LogEntry creation.""" return False + def has_change_permission(self, *args) -> bool: + """Prevent editing from django admin.""" + return False + def has_delete_permission(self, request: HttpRequest, obj: Optional[LogEntry] = None) -> bool: """Deny LogEntry deletion.""" return False @@ -60,7 +103,7 @@ class LogEntryAdmin(admin.ModelAdmin): class DeletedMessageAdmin(admin.ModelAdmin): """Admin formatting for the DeletedMessage model.""" - readonly_fields = ( + fields = ( "id", "author", "channel_id", @@ -81,96 +124,68 @@ class DeletedMessageAdmin(admin.ModelAdmin): "deletion_context__actor__id" ) - @staticmethod - def embed_data(instance: DeletedMessage) -> Optional[str]: + def embed_data(self, message: DeletedMessage) -> Optional[str]: """Format embed data in a code block for better readability.""" - if instance.embeds: + if message.embeds: return format_html( "
"
                 "{0}
", - json.dumps(instance.embeds, indent=4) + json.dumps(message.embeds, indent=4) ) + embed_data.short_description = "Embeds" + @staticmethod - def context(instance: DeletedMessage) -> str: + def context(message: DeletedMessage) -> str: """Provide full context info with a link through to context admin view.""" link = urls.reverse( "admin:api_messagedeletioncontext_change", - args=[instance.deletion_context.id] + args=[message.deletion_context.id] ) details = ( - f"Deleted by {instance.deletion_context.actor} at " - f"{instance.deletion_context.creation}" + f"Deleted by {message.deletion_context.actor} at " + f"{message.deletion_context.creation}" ) return format_html("{1}", link, details) @staticmethod - def view_full_log(instance: DeletedMessage) -> str: + def view_full_log(message: DeletedMessage) -> str: """Provide a link to the message logs for the relevant context.""" return format_html( "Click to view full context log", - instance.deletion_context.log_url + message.deletion_context.log_url ) + def has_add_permission(self, *args) -> bool: + """Prevent adding from django admin.""" + return False + + def has_change_permission(self, *args) -> bool: + """Prevent editing from django admin.""" + return False + + +class DeletedMessageInline(admin.TabularInline): + """Tabular Inline Admin model for Deleted Message to be viewed within Context.""" + + model = DeletedMessage + @admin.register(MessageDeletionContext) class MessageDeletionContextAdmin(admin.ModelAdmin): """Admin formatting for the MessageDeletionContext model.""" - readonly_fields = ("actor", "creation", "message_log") + fields = ("actor", "creation") + list_display = ("id", "creation", "actor") + inlines = (DeletedMessageInline,) - @staticmethod - def message_log(instance: MessageDeletionContext) -> str: - """Provide a formatted link to the message logs for the context.""" - return format_html( - "Click to see deleted message log", - instance.log_url - ) - - -@admin.register(Infraction) -class InfractionAdmin(admin.ModelAdmin): - """Admin formatting for the Infraction model.""" + def has_add_permission(self, *args) -> bool: + """Prevent adding from django admin.""" + return False - fields = ( - "user", - "actor", - "type", - "reason", - "inserted_at", - "expires_at", - "active", - "hidden" - ) - readonly_fields = ( - "user", - "actor", - "type", - "inserted_at" - ) - list_display = ( - "type", - "user", - "actor", - "inserted_at", - "expires_at", - "reason", - "active", - ) - search_fields = ( - "id", - "user__name", - "user__id", - "actor__name", - "actor__id", - "reason", - "type" - ) - list_filter = ( - "type", - "hidden", - "active" - ) + def has_change_permission(self, *args) -> bool: + """Prevent editing from django admin.""" + return False class NominationActorFilter(admin.SimpleListFilter): @@ -179,7 +194,7 @@ class NominationActorFilter(admin.SimpleListFilter): title = "Actor" parameter_name = "actor" - def lookups(self, request: HttpRequest, model_admin: NominationAdmin) -> Iterable[Tuple[int, str]]: + def lookups(self, request: HttpRequest, model: NominationAdmin) -> Iterable[Tuple[int, str]]: """Selectable values for viewer to filter by.""" actor_ids = Nomination.objects.order_by().values_list("actor").distinct() actors = User.objects.filter(id__in=actor_ids) @@ -322,7 +337,7 @@ class UserTopRoleFilter(admin.SimpleListFilter): title = "Role" parameter_name = "role" - def lookups(self, request: HttpRequest, model_admin: UserAdmin) -> Iterable[Tuple[str, str]]: + def lookups(self, request: HttpRequest, model: UserAdmin) -> Iterable[Tuple[str, str]]: """Selectable values for viewer to filter by.""" roles = Role.objects.all() return ((r.name, r.name) for r in roles) diff --git a/pydis_site/apps/api/models/bot/message.py b/pydis_site/apps/api/models/bot/message.py index f6ae55a5..ff06de21 100644 --- a/pydis_site/apps/api/models/bot/message.py +++ b/pydis_site/apps/api/models/bot/message.py @@ -21,7 +21,8 @@ class Message(ModelReprMixin, models.Model): limit_value=0, message="Message IDs cannot be negative." ), - ) + ), + verbose_name="ID" ) author = models.ForeignKey( User, @@ -38,7 +39,8 @@ class Message(ModelReprMixin, models.Model): limit_value=0, message="Channel IDs cannot be negative." ), - ) + ), + verbose_name="Channel ID" ) content = models.CharField( max_length=2_000, -- cgit v1.2.3 From c6e80a97bb9e40dc404e274d6ebc419b410a2b66 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 18 Sep 2020 15:16:00 +1000 Subject: Add DocumentationLink and BotSetting Admin models. --- pydis_site/apps/api/admin.py | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index ca97512f..7b571005 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -27,6 +27,28 @@ admin.site.site_header = "Python Discord | Administration" admin.site.site_title = "Python Discord" +@admin.register(BotSetting) +class BotSettingAdmin(admin.ModelAdmin): + """Admin formatting for the BotSetting model.""" + + fields = ("name", "data") + list_display = ("name",) + + def has_add_permission(self, *args) -> bool: + """Prevent adding from django admin.""" + return False + + +@admin.register(DocumentationLink) +class DocumentationLinkAdmin(admin.ModelAdmin): + """Admin formatting for the DocumentationLink model.""" + + fields = ("package", "base_url", "inventory_url") + list_display = ("package", "base_url", "inventory_url") + list_editable = ("base_url", "inventory_url") + search_fields = ("package",) + + @admin.register(Infraction) class InfractionAdmin(admin.ModelAdmin): """Admin formatting for the Infraction model.""" @@ -71,6 +93,10 @@ class InfractionAdmin(admin.ModelAdmin): "active" ) + def has_add_permission(self, *args) -> bool: + """Prevent adding from django admin.""" + return False + @admin.register(LogEntry) class LogEntryAdmin(admin.ModelAdmin): @@ -124,6 +150,8 @@ class DeletedMessageAdmin(admin.ModelAdmin): "deletion_context__actor__id" ) + list_display = ("id", "author", "channel_id") + def embed_data(self, message: DeletedMessage) -> Optional[str]: """Format embed data in a code block for better readability.""" if message.embeds: @@ -389,7 +417,3 @@ class UserAdmin(admin.ModelAdmin): def has_change_permission(self, *args) -> bool: """Prevent editing from django admin.""" return False - - -admin.site.register(BotSetting) -admin.site.register(DocumentationLink) -- cgit v1.2.3 From 8af9e190f69484efb5fe3b5910a9125738e0ee84 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 18 Sep 2020 15:22:48 +1000 Subject: Declutter Infraction admin list, add actor list filter. --- pydis_site/apps/api/admin.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index 7b571005..ff9afc46 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -49,6 +49,25 @@ class DocumentationLinkAdmin(admin.ModelAdmin): search_fields = ("package",) +class InfractionActorFilter(admin.SimpleListFilter): + """Actor Filter for Infraction Admin list page.""" + + title = "Actor" + parameter_name = "actor" + + def lookups(self, request: HttpRequest, model: NominationAdmin) -> Iterable[Tuple[int, str]]: + """Selectable values for viewer to filter by.""" + actor_ids = Infraction.objects.order_by().values_list("actor").distinct() + actors = User.objects.filter(id__in=actor_ids) + return ((a.id, a.username) for a in actors) + + def queryset(self, request: HttpRequest, queryset: QuerySet) -> Optional[QuerySet]: + """Query to filter the list of Users against.""" + if not self.value(): + return + return queryset.filter(actor__id=self.value()) + + @admin.register(Infraction) class InfractionAdmin(admin.ModelAdmin): """Admin formatting for the Infraction model.""" @@ -67,16 +86,16 @@ class InfractionAdmin(admin.ModelAdmin): "user", "actor", "type", - "inserted_at" + "inserted_at", + "active", + "hidden" ) list_display = ( "type", + "active", "user", - "actor", "inserted_at", - "expires_at", "reason", - "active", ) search_fields = ( "id", @@ -90,7 +109,8 @@ class InfractionAdmin(admin.ModelAdmin): list_filter = ( "type", "hidden", - "active" + "active", + InfractionActorFilter ) def has_add_permission(self, *args) -> bool: -- cgit v1.2.3 From 2c4b3451b3349d098ceebce774b08946b5e378d5 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 18 Sep 2020 15:24:54 +1000 Subject: Don't allow expiry to be editable, due to pending bot tasks unsyncing --- pydis_site/apps/api/admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index ff9afc46..6267b7a8 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -87,6 +87,7 @@ class InfractionAdmin(admin.ModelAdmin): "actor", "type", "inserted_at", + "expires_at", "active", "hidden" ) -- cgit v1.2.3 From 8edeb88f92b8ed48f2f383c2245416486f8e99cc Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sat, 19 Sep 2020 01:23:58 +1000 Subject: Remove noqa from DeletedMessagesLogURLTests setup. --- pydis_site/apps/api/tests/test_deleted_messages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/apps/api/tests/test_deleted_messages.py b/pydis_site/apps/api/tests/test_deleted_messages.py index 287c1737..448f1186 100644 --- a/pydis_site/apps/api/tests/test_deleted_messages.py +++ b/pydis_site/apps/api/tests/test_deleted_messages.py @@ -81,7 +81,7 @@ class DeletedMessagesWithActorTests(APISubdomainTestCase): class DeletedMessagesLogURLTests(APISubdomainTestCase): @classmethod - def setUpTestData(cls): # noqa + def setUpTestData(cls): cls.author = cls.actor = User.objects.create( id=324888, name='Black Knight', -- cgit v1.2.3 From 520ea636a3b22beb3fb153dea5e6dd0dd6f809f4 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sat, 19 Sep 2020 02:23:01 +1000 Subject: Update user model in DeletedMessagesLogURLTests. `avatar_hash` is no longer a field stored in the database. --- pydis_site/apps/api/tests/test_deleted_messages.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pydis_site/apps/api/tests/test_deleted_messages.py b/pydis_site/apps/api/tests/test_deleted_messages.py index 448f1186..40450844 100644 --- a/pydis_site/apps/api/tests/test_deleted_messages.py +++ b/pydis_site/apps/api/tests/test_deleted_messages.py @@ -86,7 +86,6 @@ class DeletedMessagesLogURLTests(APISubdomainTestCase): id=324888, name='Black Knight', discriminator=1975, - avatar_hash=None ) cls.deletion_context = MessageDeletionContext.objects.create( -- cgit v1.2.3 From 394d1ca151ad28be431eff8f25dde5707eeb0d47 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sun, 20 Sep 2020 00:04:20 +1000 Subject: Test username property formatting for user model. --- pydis_site/apps/api/tests/test_users.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py index 4c0f6e27..a02fce8a 100644 --- a/pydis_site/apps/api/tests/test_users.py +++ b/pydis_site/apps/api/tests/test_users.py @@ -143,7 +143,7 @@ class UserModelTests(APISubdomainTestCase): cls.user_with_roles = User.objects.create( id=1, name="Test User with two roles", - discriminator=1111, + discriminator=1, in_guild=True, ) cls.user_with_roles.roles.extend([cls.role_bottom.id, cls.role_top.id]) @@ -166,3 +166,7 @@ class UserModelTests(APISubdomainTestCase): top_role = self.user_without_roles.top_role self.assertIsInstance(top_role, Role) self.assertEqual(top_role.id, self.developers_role.id) + + def test_correct_username_formatting(self): + """Tests the username property with both name and discriminator formatted together.""" + self.assertEqual(self.user_with_roles.username, "Test User with two roles#0001") -- cgit v1.2.3 From dafca0fee8ac618ee9aab5ce8a1d7c34aafc3a05 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sun, 20 Sep 2020 02:06:11 +1000 Subject: Change UserTopRoleFilter to UserRoleFilter. Filter checks for general role membership instead of only those who have the selected role as top role. Noticed during development that we'd not be able to filter to show all Helpers otherwise, as some Helpers have different top roles such as Core Dev that wouldn't give immediately obvious behaviour to user expectations. --- pydis_site/apps/api/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index 6267b7a8..733a056d 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -380,7 +380,7 @@ class RoleAdmin(admin.ModelAdmin): return False -class UserTopRoleFilter(admin.SimpleListFilter): +class UserRoleFilter(admin.SimpleListFilter): """List Filter for User list Admin page.""" title = "Role" @@ -426,7 +426,7 @@ class UserAdmin(admin.ModelAdmin): all_roles_coloured.short_description = "All Roles" search_fields = ("name", "id", "roles") - list_filter = (UserTopRoleFilter, "in_guild") + list_filter = (UserRoleFilter, "in_guild") list_display = ("username", "top_role_coloured", "in_guild") fields = ("username", "id", "in_guild", "all_roles_coloured") sortable_by = ("username",) -- cgit v1.2.3 From 715dd46aa68358cdd2abee07b018ee08e54da8d9 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sun, 20 Sep 2020 05:02:48 +1000 Subject: Allow Nomination end_reason to have a blank value for validation. Without `blank=True`, admin page editable forms could not be saved if no content was in the end_reason input. --- pydis_site/apps/api/models/bot/nomination.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py index 54f56c98..11b9e36e 100644 --- a/pydis_site/apps/api/models/bot/nomination.py +++ b/pydis_site/apps/api/models/bot/nomination.py @@ -34,7 +34,8 @@ class Nomination(ModelReprMixin, models.Model): ) end_reason = models.TextField( help_text="Why the nomination was ended.", - default="" + default="", + blank=True ) ended_at = models.DateTimeField( auto_now_add=False, -- cgit v1.2.3 From d39eeb9b7e936f264abcfbb998453179ef8556f5 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sun, 20 Sep 2020 06:27:16 +1000 Subject: Change Infraction admin to use fieldsets for better grouping of info. --- pydis_site/apps/api/admin.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index 733a056d..c3f1179e 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -72,15 +72,11 @@ class InfractionActorFilter(admin.SimpleListFilter): class InfractionAdmin(admin.ModelAdmin): """Admin formatting for the Infraction model.""" - fields = ( - "user", - "actor", - "type", - "reason", - "inserted_at", - "expires_at", - "active", - "hidden" + fieldsets = ( + ("Members", {"fields": ("user", "actor")}), + ("Action", {"fields": ("type", "hidden", "active")}), + ("Dates", {"fields": ("inserted_at", "expires_at")}), + ("Reason", {"fields": ("reason",)}), ) readonly_fields = ( "user", -- cgit v1.2.3 From 9344d8ac63df05d67a79fbeacc7a8c31acf86853 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sun, 20 Sep 2020 06:35:38 +1000 Subject: Change documentation link model to order by package. --- pydis_site/apps/api/models/bot/documentation_link.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pydis_site/apps/api/models/bot/documentation_link.py b/pydis_site/apps/api/models/bot/documentation_link.py index 5a46460b..2a0ce751 100644 --- a/pydis_site/apps/api/models/bot/documentation_link.py +++ b/pydis_site/apps/api/models/bot/documentation_link.py @@ -24,3 +24,8 @@ class DocumentationLink(ModelReprMixin, models.Model): def __str__(self): """Returns the package and URL for the current documentation link, for display purposes.""" return f"{self.package} - {self.base_url}" + + class Meta: + """Defines the meta options for the documentation link model.""" + + ordering = ['package'] -- cgit v1.2.3 From 1916074a704fc6f4b8595b8afb5463436b60961a Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sun, 20 Sep 2020 06:36:30 +1000 Subject: Add migrations for nomination and doc link model changes. --- .../apps/api/migrations/0064_auto_20200919_1900.py | 76 ++++++++++++++++++++++ .../apps/api/migrations/0065_auto_20200919_2033.py | 17 +++++ 2 files changed, 93 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0064_auto_20200919_1900.py create mode 100644 pydis_site/apps/api/migrations/0065_auto_20200919_2033.py diff --git a/pydis_site/apps/api/migrations/0064_auto_20200919_1900.py b/pydis_site/apps/api/migrations/0064_auto_20200919_1900.py new file mode 100644 index 00000000..0080eb42 --- /dev/null +++ b/pydis_site/apps/api/migrations/0064_auto_20200919_1900.py @@ -0,0 +1,76 @@ +# Generated by Django 3.0.9 on 2020-09-19 19:00 + +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.offensive_message + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0063_Allow_blank_or_null_for_nomination_reason'), + ] + + operations = [ + migrations.AlterModelOptions( + name='deletedmessage', + options={'ordering': ('-id',)}, + ), + migrations.AlterModelOptions( + name='messagedeletioncontext', + options={'ordering': ('-creation',)}, + ), + migrations.AlterModelOptions( + name='nomination', + options={'ordering': ('-inserted_at',)}, + ), + migrations.AlterModelOptions( + name='role', + options={'ordering': ('-position',)}, + ), + migrations.AlterField( + model_name='deletedmessage', + name='channel_id', + field=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.')], verbose_name='Channel ID'), + ), + migrations.AlterField( + model_name='deletedmessage', + name='id', + field=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.')], verbose_name='ID'), + ), + migrations.AlterField( + model_name='nomination', + name='end_reason', + field=models.TextField(blank=True, default='', help_text='Why the nomination was ended.'), + ), + migrations.AlterField( + model_name='offensivemessage', + name='channel_id', + field=models.BigIntegerField(help_text='The channel ID that the message was sent in, taken from Discord.', validators=[django.core.validators.MinValueValidator(limit_value=0, message='Channel IDs cannot be negative.')], verbose_name='Channel ID'), + ), + migrations.AlterField( + model_name='offensivemessage', + name='delete_date', + field=models.DateTimeField(help_text='The date on which the message will be auto-deleted.', validators=[pydis_site.apps.api.models.bot.offensive_message.future_date_validator], verbose_name='To Be Deleted'), + ), + migrations.AlterField( + model_name='offensivemessage', + name='id', + field=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.')], verbose_name='Message ID'), + ), + migrations.AlterField( + model_name='role', + name='id', + field=models.BigIntegerField(help_text='The role ID, taken from Discord.', primary_key=True, serialize=False, validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')], verbose_name='ID'), + ), + migrations.AlterField( + model_name='user', + name='id', + field=models.BigIntegerField(help_text='The ID of this user, taken from Discord.', primary_key=True, serialize=False, validators=[django.core.validators.MinValueValidator(limit_value=0, message='User IDs cannot be negative.')], verbose_name='ID'), + ), + migrations.AlterField( + model_name='user', + name='in_guild', + field=models.BooleanField(default=True, help_text='Whether this user is in our server.', verbose_name='In Guild'), + ), + ] diff --git a/pydis_site/apps/api/migrations/0065_auto_20200919_2033.py b/pydis_site/apps/api/migrations/0065_auto_20200919_2033.py new file mode 100644 index 00000000..89bc4e02 --- /dev/null +++ b/pydis_site/apps/api/migrations/0065_auto_20200919_2033.py @@ -0,0 +1,17 @@ +# Generated by Django 3.0.9 on 2020-09-19 20:33 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0064_auto_20200919_1900'), + ] + + operations = [ + migrations.AlterModelOptions( + name='documentationlink', + options={'ordering': ['package']}, + ), + ] -- cgit v1.2.3 From a105de702d2f64a66acfc2f06a3b994cd46a5c3e Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sun, 20 Sep 2020 07:06:40 +1000 Subject: Remove delete permission for bot settings admin. I'm unable to see any cases where this would be wanted, and instead accidental deletion would result in the system possibly breaking, as we are unable to add the setting again to replace it if it got removed. The name has also set to read only in item view, to prevent renames, effectively doing the same thing as deleting it. --- pydis_site/apps/api/admin.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index c3f1179e..5093e605 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -33,11 +33,16 @@ class BotSettingAdmin(admin.ModelAdmin): fields = ("name", "data") list_display = ("name",) + readonly_fields = ("name",) def has_add_permission(self, *args) -> bool: """Prevent adding from django admin.""" return False + def has_delete_permission(self, *args) -> bool: + """Prevent deleting from django admin.""" + return False + @admin.register(DocumentationLink) class DocumentationLinkAdmin(admin.ModelAdmin): -- cgit v1.2.3 From 48fcd02fc53f5ab10a4c03458a2bc0db778b54ea Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Tue, 22 Sep 2020 02:11:22 +0530 Subject: optimize bulk update endpoint by using Model.objects.bulk_update() method The Model.objects.bulk_update() method greatly reduces the number of SQL queries by updating all required instances in 1 query. --- pydis_site/apps/api/serializers.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index ae57b307..1f24d29f 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -281,10 +281,16 @@ class UserListSerializer(ListSerializer): data_mapping = {item['id']: item for item in validated_data} updated = [] + fields_to_update = set() for user_id, data in data_mapping.items(): + for key in data: + fields_to_update.add(key) user = instance_mapping.get(user_id) - updated.append(self.child.update(user, data)) + user.__dict__.update(data) + updated.append(user) + fields_to_update.remove("id") + User.objects.bulk_update(updated, fields_to_update) return updated -- cgit v1.2.3 From 9a98122a2544fc943b05015b156769ce2a53d0b6 Mon Sep 17 00:00:00 2001 From: Den4200 Date: Sat, 26 Sep 2020 20:13:06 -0400 Subject: Add "Welcome to Python Discord" video to index --- pydis_site/templates/home/index.html | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pydis_site/templates/home/index.html b/pydis_site/templates/home/index.html index 3e96cc91..f31363a4 100644 --- a/pydis_site/templates/home/index.html +++ b/pydis_site/templates/home/index.html @@ -39,9 +39,7 @@ {# Right column container #}
- - - +
-- cgit v1.2.3 From 7a12a372cc0202a208e794b36774b9e56d2be096 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Thu, 1 Oct 2020 19:45:44 +0100 Subject: Update rule 5 --- pydis_site/apps/api/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pydis_site/apps/api/views.py b/pydis_site/apps/api/views.py index 7ac56641..0d126051 100644 --- a/pydis_site/apps/api/views.py +++ b/pydis_site/apps/api/views.py @@ -135,8 +135,9 @@ class RulesView(APIView): ), ( "Do not provide or request help on projects that may break laws, " - "breach terms of services, be considered malicious/inappropriate " - "or be for graded coursework/exams." + "breach terms of services, be considered malicious or inappropriate. " + "Do not help with ongoing exams. Do not provide or request solutions " + "for graded assignments, although general guidance is okay." ), ( "No spamming or unapproved advertising, including requests for paid work. " -- cgit v1.2.3 From 3e4661e984d85d4df6c2e12655bbde078efbaeb8 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Fri, 2 Oct 2020 23:45:27 +0100 Subject: Only request the identify scope with Discord on OAuth2 login --- pydis_site/settings.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pydis_site/settings.py b/pydis_site/settings.py index 1f042c1b..eee559e4 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -401,3 +401,10 @@ ACCOUNT_USERNAME_VALIDATORS = "pydis_site.VALIDATORS" LOGIN_REDIRECT_URL = "home" SOCIALACCOUNT_ADAPTER = "pydis_site.utils.account.SocialAccountAdapter" +SOCIALACCOUNT_PROVIDERS = { + 'discord': { + 'SCOPE': [ + 'identify', + ], + } +} -- cgit v1.2.3 From 4b07fe054215d9cfd5a66214761c74608f6448d5 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sat, 3 Oct 2020 00:04:28 +0100 Subject: Display no prompt to users who have already authenticated --- pydis_site/settings.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pydis_site/settings.py b/pydis_site/settings.py index eee559e4..3769fa25 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -402,9 +402,10 @@ ACCOUNT_USERNAME_VALIDATORS = "pydis_site.VALIDATORS" LOGIN_REDIRECT_URL = "home" SOCIALACCOUNT_ADAPTER = "pydis_site.utils.account.SocialAccountAdapter" SOCIALACCOUNT_PROVIDERS = { - 'discord': { - 'SCOPE': [ - 'identify', + "discord": { + "SCOPE": [ + "identify", ], + "AUTH_PARAMS": {"prompt": "none"} } } -- cgit v1.2.3 From b1fe351399f3ec70d82cb312d5f791aa27fcdbdf Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 3 Oct 2020 09:40:53 +0300 Subject: Logs Cleanup: Remove logs admin --- pydis_site/apps/api/admin.py | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index 5093e605..b6fee9d1 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -14,7 +14,6 @@ from .models import ( DeletedMessage, DocumentationLink, Infraction, - LogEntry, MessageDeletionContext, Nomination, OffTopicChannelName, @@ -120,33 +119,6 @@ class InfractionAdmin(admin.ModelAdmin): return False -@admin.register(LogEntry) -class LogEntryAdmin(admin.ModelAdmin): - """Allows viewing logs in the Django Admin without allowing edits.""" - - actions = None - list_display = ('timestamp', 'level', 'message') - fieldsets = ( - ('Overview', {'fields': ('timestamp', 'application', 'logger_name')}), - ('Metadata', {'fields': ('level', 'module', 'line')}), - ('Contents', {'fields': ('message',)}) - ) - list_filter = ('level', 'timestamp') - search_fields = ('message',) - - def has_add_permission(self, request: HttpRequest) -> bool: - """Deny manual LogEntry creation.""" - return False - - def has_change_permission(self, *args) -> bool: - """Prevent editing from django admin.""" - return False - - def has_delete_permission(self, request: HttpRequest, obj: Optional[LogEntry] = None) -> bool: - """Deny LogEntry deletion.""" - return False - - @admin.register(DeletedMessage) class DeletedMessageAdmin(admin.ModelAdmin): """Admin formatting for the DeletedMessage model.""" -- cgit v1.2.3 From e4d1803791ee007086fe9e015e1d57745addeeb4 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 3 Oct 2020 09:42:29 +0300 Subject: Logs Cleanup: Remove logs viewset --- pydis_site/apps/api/viewsets/__init__.py | 1 - pydis_site/apps/api/viewsets/log_entry.py | 36 ------------------------------- 2 files changed, 37 deletions(-) delete mode 100644 pydis_site/apps/api/viewsets/log_entry.py diff --git a/pydis_site/apps/api/viewsets/__init__.py b/pydis_site/apps/api/viewsets/__init__.py index dfbb880d..f133e77f 100644 --- a/pydis_site/apps/api/viewsets/__init__.py +++ b/pydis_site/apps/api/viewsets/__init__.py @@ -12,4 +12,3 @@ from .bot import ( RoleViewSet, UserViewSet ) -from .log_entry import LogEntryViewSet diff --git a/pydis_site/apps/api/viewsets/log_entry.py b/pydis_site/apps/api/viewsets/log_entry.py deleted file mode 100644 index 9108a4fa..00000000 --- a/pydis_site/apps/api/viewsets/log_entry.py +++ /dev/null @@ -1,36 +0,0 @@ -from rest_framework.mixins import CreateModelMixin -from rest_framework.viewsets import GenericViewSet - -from pydis_site.apps.api.models.log_entry import LogEntry -from pydis_site.apps.api.serializers import LogEntrySerializer - - -class LogEntryViewSet(CreateModelMixin, GenericViewSet): - """ - View supporting the creation of log entries in the database for viewing via the log browser. - - ## Routes - ### POST /logs - Create a new log entry. - - #### Request body - >>> { - ... 'application': str, # 'bot' | 'seasonalbot' | 'site' - ... 'logger_name': str, # such as 'bot.cogs.moderation' - ... 'timestamp': Optional[str], # from `datetime.utcnow().isoformat()` - ... 'level': str, # 'debug' | 'info' | 'warning' | 'error' | 'critical' - ... 'module': str, # such as 'pydis_site.apps.api.serializers' - ... 'line': int, # > 0 - ... 'message': str, # textual formatted content of the logline - ... } - - #### Status codes - - 201: returned on success - - 400: if the request body has invalid fields, see the response for details - - ## Authentication - Requires a API token. - """ - - queryset = LogEntry.objects.all() - serializer_class = LogEntrySerializer -- cgit v1.2.3 From f0d4982b48fe6c6998564d6ca3f10a3a5fc16d77 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 3 Oct 2020 09:46:01 +0300 Subject: Logs Cleanup: Remove logs URL --- pydis_site/apps/api/urls.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pydis_site/apps/api/urls.py b/pydis_site/apps/api/urls.py index 4dbf93db..2e1ef0b4 100644 --- a/pydis_site/apps/api/urls.py +++ b/pydis_site/apps/api/urls.py @@ -8,7 +8,6 @@ from .viewsets import ( DocumentationLinkViewSet, FilterListViewSet, InfractionViewSet, - LogEntryViewSet, NominationViewSet, OffTopicChannelNameViewSet, OffensiveMessageViewSet, @@ -71,7 +70,6 @@ urlpatterns = ( # # from django_hosts.resolvers import reverse path('bot/', include((bot_router.urls, 'api'), namespace='bot')), - path('logs', LogEntryViewSet.as_view({'post': 'create'}), name='logs'), path('healthcheck', HealthcheckView.as_view(), name='healthcheck'), path('rules', RulesView.as_view(), name='rules') ) -- cgit v1.2.3 From d5f20aad49c24ab32af4cb61fb8fc8daf90018bc Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 3 Oct 2020 09:47:59 +0300 Subject: Logs Cleanup: Remove logs serializers --- pydis_site/apps/api/serializers.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 90bd6f91..86c4cc4b 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -191,19 +191,6 @@ class ExpandedInfractionSerializer(InfractionSerializer): return ret -class LogEntrySerializer(ModelSerializer): - """A class providing (de-)serialization of `LogEntry` instances.""" - - class Meta: - """Metadata defined for the Django REST Framework.""" - - model = LogEntry - fields = ( - 'application', 'logger_name', 'timestamp', - 'level', 'module', 'line', 'message' - ) - - class OffTopicChannelNameSerializer(ModelSerializer): """A class providing (de-)serialization of `OffTopicChannelName` instances.""" -- cgit v1.2.3 From 33d3d1abce8f6e41772021fb1be5d459d5717046 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 3 Oct 2020 09:50:17 +0300 Subject: Logs Cleanup: Remove site internal logging to DB --- pydis_site/apps/api/dblogger.py | 22 ---------------------- pydis_site/apps/api/tests/test_dblogger.py | 27 --------------------------- pydis_site/settings.py | 3 --- 3 files changed, 52 deletions(-) delete mode 100644 pydis_site/apps/api/dblogger.py delete mode 100644 pydis_site/apps/api/tests/test_dblogger.py diff --git a/pydis_site/apps/api/dblogger.py b/pydis_site/apps/api/dblogger.py deleted file mode 100644 index 4b4e3a9d..00000000 --- a/pydis_site/apps/api/dblogger.py +++ /dev/null @@ -1,22 +0,0 @@ -from logging import LogRecord, StreamHandler - - -class DatabaseLogHandler(StreamHandler): - """Logs entries into the database.""" - - def emit(self, record: LogRecord) -> None: - """Write the given `record` into the database.""" - # This import needs to be deferred due to Django's application - # registry instantiation logic loading this handler before the - # application is ready. - from pydis_site.apps.api.models.log_entry import LogEntry - - entry = LogEntry( - application='site', - logger_name=record.name, - level=record.levelname.lower(), - module=record.module, - line=record.lineno, - message=self.format(record) - ) - entry.save() diff --git a/pydis_site/apps/api/tests/test_dblogger.py b/pydis_site/apps/api/tests/test_dblogger.py deleted file mode 100644 index bb19f297..00000000 --- a/pydis_site/apps/api/tests/test_dblogger.py +++ /dev/null @@ -1,27 +0,0 @@ -import logging -from datetime import datetime - -from django.test import TestCase - -from ..dblogger import DatabaseLogHandler -from ..models import LogEntry - - -class DatabaseLogHandlerTests(TestCase): - def test_logs_to_database(self): - module_basename = __name__.split('.')[-1] - logger = logging.getLogger(__name__) - logger.handlers = [DatabaseLogHandler()] - logger.warning("I am a test case!") - - # Ensure we only have a single record in the database - # after the logging call above. - [entry] = LogEntry.objects.all() - - self.assertEqual(entry.application, 'site') - self.assertEqual(entry.logger_name, __name__) - self.assertIsInstance(entry.timestamp, datetime) - self.assertEqual(entry.level, 'warning') - self.assertEqual(entry.module, module_basename) - self.assertIsInstance(entry.line, int) - self.assertEqual(entry.message, "I am a test case!") diff --git a/pydis_site/settings.py b/pydis_site/settings.py index 3769fa25..b2e1e7f4 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -260,9 +260,6 @@ LOGGING = { 'handlers': { 'console': { 'class': 'logging.StreamHandler' - }, - 'database': { - 'class': 'pydis_site.apps.api.dblogger.DatabaseLogHandler' } }, 'loggers': { -- cgit v1.2.3 From 28300128516369de2f235cc7d0427ebceb6aa28d Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 3 Oct 2020 10:09:26 +0300 Subject: Logs Cleanup: Remove another entry of site internal logging to DB --- pydis_site/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/settings.py b/pydis_site/settings.py index b2e1e7f4..5eb812ac 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -264,7 +264,7 @@ LOGGING = { }, 'loggers': { 'django': { - 'handlers': ['console', 'database'], + 'handlers': ['console'], 'propagate': True, 'level': env( 'LOG_LEVEL', -- cgit v1.2.3 From c262b80c24450b7f2b24eb51b994495107e1aac6 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 3 Oct 2020 10:09:46 +0300 Subject: Logs Cleanup: Remove log entry model --- pydis_site/apps/api/models/__init__.py | 1 - pydis_site/apps/api/models/log_entry.py | 55 --------------------------------- 2 files changed, 56 deletions(-) delete mode 100644 pydis_site/apps/api/models/log_entry.py diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py index e3f928e1..0a8c90f6 100644 --- a/pydis_site/apps/api/models/__init__.py +++ b/pydis_site/apps/api/models/__init__.py @@ -14,4 +14,3 @@ from .bot import ( Role, User ) -from .log_entry import LogEntry diff --git a/pydis_site/apps/api/models/log_entry.py b/pydis_site/apps/api/models/log_entry.py deleted file mode 100644 index 752cd2ca..00000000 --- a/pydis_site/apps/api/models/log_entry.py +++ /dev/null @@ -1,55 +0,0 @@ -from django.db import models -from django.utils import timezone - -from pydis_site.apps.api.models.mixins import ModelReprMixin - - -class LogEntry(ModelReprMixin, models.Model): - """A log entry generated by one of the PyDis applications.""" - - application = models.CharField( - max_length=20, - help_text="The application that generated this log entry.", - choices=( - ('bot', 'Bot'), - ('seasonalbot', 'Seasonalbot'), - ('site', 'Website') - ) - ) - logger_name = models.CharField( - max_length=100, - help_text="The name of the logger that generated this log entry." - ) - timestamp = models.DateTimeField( - default=timezone.now, - help_text="The date and time when this entry was created." - ) - level = models.CharField( - max_length=8, # 'critical' - choices=( - ('debug', 'Debug'), - ('info', 'Info'), - ('warning', 'Warning'), - ('error', 'Error'), - ('critical', 'Critical') - ), - help_text=( - "The logger level at which this entry was emitted. The levels " - "correspond to the Python `logging` levels." - ) - ) - module = models.CharField( - max_length=100, - help_text="The fully qualified path of the module generating this log line." - ) - line = models.PositiveSmallIntegerField( - help_text="The line at which the log line was emitted." - ) - message = models.TextField( - help_text="The textual content of the log line." - ) - - class Meta: - """Customizes the default generated plural name to valid English.""" - - verbose_name_plural = 'Log entries' -- cgit v1.2.3 From e40fc796bf867143e2c73a4f7b6bf90a1b63fe76 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 3 Oct 2020 10:10:12 +0300 Subject: Logs Cleanup: Add migration for log entry removal --- pydis_site/apps/api/migrations/0064_delete_logentry.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0064_delete_logentry.py diff --git a/pydis_site/apps/api/migrations/0064_delete_logentry.py b/pydis_site/apps/api/migrations/0064_delete_logentry.py new file mode 100644 index 00000000..a5f344d1 --- /dev/null +++ b/pydis_site/apps/api/migrations/0064_delete_logentry.py @@ -0,0 +1,16 @@ +# Generated by Django 3.0.9 on 2020-10-03 06:57 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0063_Allow_blank_or_null_for_nomination_reason'), + ] + + operations = [ + migrations.DeleteModel( + name='LogEntry', + ), + ] -- cgit v1.2.3 From be671afdfc296e67e74791ded1674d2ce00af364 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 3 Oct 2020 10:19:32 +0300 Subject: Logs Cleanup: Remove removed LogEntry model import --- pydis_site/apps/api/serializers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 86c4cc4b..f9a5517e 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -9,7 +9,6 @@ from .models import ( DocumentationLink, FilterList, Infraction, - LogEntry, MessageDeletionContext, Nomination, OffTopicChannelName, -- cgit v1.2.3 From 9aca77b6853cd106d265404f017d4697d04d1b25 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 3 Oct 2020 10:31:04 +0300 Subject: Logs Cleanup: Fix migrations conflicts with merge migration --- pydis_site/apps/api/migrations/0066_merge_20201003_0730.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0066_merge_20201003_0730.py diff --git a/pydis_site/apps/api/migrations/0066_merge_20201003_0730.py b/pydis_site/apps/api/migrations/0066_merge_20201003_0730.py new file mode 100644 index 00000000..298416db --- /dev/null +++ b/pydis_site/apps/api/migrations/0066_merge_20201003_0730.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.9 on 2020-10-03 07:30 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0064_delete_logentry'), + ('api', '0065_auto_20200919_2033'), + ] + + operations = [ + ] -- cgit v1.2.3 From d8de1be9906a19b79b172b61f4b96189497c6ee1 Mon Sep 17 00:00:00 2001 From: imSofi <20756843+imsofi@users.noreply.github.com> Date: Mon, 5 Oct 2020 02:58:19 +0200 Subject: Bump links for Effective Python to 2nd edition Updated and expanded book for Python 3. Has 30 new major guidelines added compared to the 1st edition. --- pydis_site/apps/home/resources/books/effective_python.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pydis_site/apps/home/resources/books/effective_python.yaml b/pydis_site/apps/home/resources/books/effective_python.yaml index ab782704..7f9d0dea 100644 --- a/pydis_site/apps/home/resources/books/effective_python.yaml +++ b/pydis_site/apps/home/resources/books/effective_python.yaml @@ -1,4 +1,4 @@ -description: A book that gives 59 best practices for writing excellent Python. Great +description: A book that gives 90 best practices for writing excellent Python. Great for intermediates. name: Effective Python payment: paid @@ -8,7 +8,7 @@ urls: url: https://effectivepython.com/ - icon: branding/amazon title: Amazon - url: https://www.amazon.com/Effective-Python-Specific-Software-Development/dp/0134034287 + url: https://www.amazon.com/Effective-Python-Specific-Software-Development/dp/0134853989 - icon: branding/github title: GitHub url: https://github.com/bslatkin/effectivepython -- cgit v1.2.3 From e7a61d0a2c96f48c7495f1f25e56384f6dbac645 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Mon, 5 Oct 2020 18:18:09 +0530 Subject: raise validation error for user not found in UserListSerializer --- pydis_site/apps/api/serializers.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 1f24d29f..a560d491 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -278,16 +278,20 @@ class UserListSerializer(ListSerializer): ref:https://www.django-rest-framework.org/api-guide/serializers/#customizing-multiple-update """ instance_mapping = {user.id: user for user in instance} - data_mapping = {item['id']: item for item in validated_data} updated = [] fields_to_update = set() - for user_id, data in data_mapping.items(): - for key in data: + for user_data in validated_data: + for key in user_data: fields_to_update.add(key) - user = instance_mapping.get(user_id) - user.__dict__.update(data) - updated.append(user) + + try: + user = instance_mapping[user_data["id"]] + except KeyError: + raise ValidationError({"id": f"User with id {user_data['id']} not found."}) + + user.__dict__.update(user_data) + updated.append(user) fields_to_update.remove("id") User.objects.bulk_update(updated, fields_to_update) @@ -297,6 +301,7 @@ class UserListSerializer(ListSerializer): class UserSerializer(ModelSerializer): """A class providing (de-)serialization of `User` instances.""" + # ID field must be explicitly set as the default id field is read-only. id = IntegerField(min_value=0) class Meta: -- cgit v1.2.3 From a95603405a1f7d80845fdf8e4719ce7f4c385594 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Mon, 5 Oct 2020 18:19:45 +0530 Subject: return next and previous page numbers in paginator instead of links --- pydis_site/apps/api/viewsets/bot/user.py | 41 +++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index d015fe71..0dd529be 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -1,3 +1,5 @@ +from collections import OrderedDict + from rest_framework import status from rest_framework.decorators import action from rest_framework.pagination import PageNumberPagination @@ -16,6 +18,30 @@ class UserListPagination(PageNumberPagination): page_size = 10000 page_size_query_param = "page_size" + def get_next_page_number(self) -> int: + """Get the next page number.""" + if not self.page.has_next(): + return None + page_number = self.page.next_page_number() + return page_number + + def get_previous_page_number(self) -> int: + """Get the previous page number.""" + if not self.page.has_previous(): + return None + + page_number = self.page.previous_page_number() + return page_number + + def get_paginated_response(self, data: list) -> Response: + """Override method to send modified response.""" + return Response(OrderedDict([ + ('count', self.page.paginator.count), + ('next_page_no', self.get_next_page_number()), + ('previous_page_no', self.get_previous_page_number()), + ('results', data) + ])) + class UserViewSet(ModelViewSet): """ @@ -193,13 +219,6 @@ class UserViewSet(ModelViewSet): filtered_instances = queryset.filter(id__in=object_ids) - if filtered_instances.count() != len(object_ids): - # If all user objects passed in request.body are not present in the database. - resp = { - "Error": "User object not found." - } - return Response(resp, status=status.HTTP_404_NOT_FOUND) - serializer = self.get_serializer( instance=filtered_instances, data=request.data, @@ -207,7 +226,7 @@ class UserViewSet(ModelViewSet): partial=True ) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + serializer.is_valid(raise_exception=True) + serializer.save() + + return Response(serializer.data, status=status.HTTP_200_OK) -- cgit v1.2.3 From 69342e8cc952ff2dd25ac50f7a80d30e6d409a0f Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Mon, 5 Oct 2020 18:20:43 +0530 Subject: Change test case to use updated code --- pydis_site/apps/api/tests/test_users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py index 1f9bd687..affc2c48 100644 --- a/pydis_site/apps/api/tests/test_users.py +++ b/pydis_site/apps/api/tests/test_users.py @@ -213,7 +213,7 @@ class MultiPatchTests(APISubdomainTestCase): } ] response = self.client.patch(url, data=data) - self.assertEqual(response.status_code, 404) + self.assertEqual(response.status_code, 400) def test_returns_400_for_bad_data(self): url = reverse("bot:user-bulk-patch", host="api") -- cgit v1.2.3 From 3eee5e76894995cea87391d60c9749eb66b3187a Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Tue, 6 Oct 2020 21:21:53 +0530 Subject: Update docstring of UserViewSet and correct function annotation on UserListPagination --- pydis_site/apps/api/viewsets/bot/user.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 0dd529be..ebea6a45 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -1,3 +1,4 @@ +import typing from collections import OrderedDict from rest_framework import status @@ -18,14 +19,14 @@ class UserListPagination(PageNumberPagination): page_size = 10000 page_size_query_param = "page_size" - def get_next_page_number(self) -> int: + def get_next_page_number(self) -> typing.Optional[int]: """Get the next page number.""" if not self.page.has_next(): return None page_number = self.page.next_page_number() return page_number - def get_previous_page_number(self) -> int: + def get_previous_page_number(self) -> typing.Optional[int]: """Get the previous page number.""" if not self.page.has_previous(): return None @@ -54,8 +55,8 @@ class UserViewSet(ModelViewSet): #### Response format >>> { ... 'count': 95000, - ... 'next': "http://api.pythondiscord.com/bot/users?page=2", - ... 'previous': None, + ... 'next_page_no': "2", + ... 'previous_page_no': None, ... 'results': [ ... { ... 'id': 409107086526644234, -- cgit v1.2.3 From 6b28ac6eec610871753d63fa95ccc7311c759181 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Tue, 6 Oct 2020 21:24:50 +0530 Subject: overhaul create method to ignore conflicts and raise error when duplicates present in request data. Raising NotFound exception instead of ValidatoinError in update method --- pydis_site/apps/api/serializers.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index a560d491..51afbe73 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -1,5 +1,6 @@ """Converters from Django models to data interchange formats and back.""" from django.db.models.query import QuerySet +from rest_framework.exceptions import NotFound from rest_framework.serializers import ( IntegerField, ListSerializer, @@ -260,16 +261,16 @@ class UserListSerializer(ListSerializer): def create(self, validated_data: list) -> list: """Override create method to optimize django queries.""" - present_users = User.objects.all() new_users = [] - present_user_ids = [user.id for user in present_users] + request_user_ids = [user["id"] for user in validated_data] for user_dict in validated_data: - if user_dict["id"] in present_user_ids: - raise ValidationError({"id": "User already exists."}) + if request_user_ids.count(user_dict["id"]) > 1: + raise ValidationError({"id": f"User with ID {user_dict['id']} " + f"given multiple times."}) new_users.append(User(**user_dict)) - return User.objects.bulk_create(new_users) + return User.objects.bulk_create(new_users, ignore_conflicts=True) def update(self, instance: QuerySet, validated_data: list) -> list: """ @@ -288,7 +289,7 @@ class UserListSerializer(ListSerializer): try: user = instance_mapping[user_data["id"]] except KeyError: - raise ValidationError({"id": f"User with id {user_data['id']} not found."}) + raise NotFound({"id": f"User with id {user_data['id']} not found."}) user.__dict__.update(user_data) updated.append(user) -- cgit v1.2.3 From 3c63323deea17a98a5c4ad899d868c9171edbacb Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Tue, 6 Oct 2020 21:26:04 +0530 Subject: refactor tests to use updated changes and add tests for UserListPagination --- pydis_site/apps/api/tests/test_users.py | 66 ++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py index affc2c48..fb22c627 100644 --- a/pydis_site/apps/api/tests/test_users.py +++ b/pydis_site/apps/api/tests/test_users.py @@ -123,7 +123,7 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response.status_code, 400) def test_returns_400_for_user_recreation(self): - """Return 400 if User is already present in database.""" + """Return 201 if User is already present in database as it skips User creation.""" url = reverse('bot:user-list', host='api') data = [{ 'id': 11, @@ -132,6 +132,26 @@ class CreationTests(APISubdomainTestCase): 'in_guild': True }] response = self.client.post(url, data=data) + self.assertEqual(response.status_code, 201) + + def test_returns_400_for_duplicate_request_users(self): + """Return 400 if 2 Users with same ID is passed in the request data.""" + url = reverse('bot:user-list', host='api') + data = [ + { + 'id': 11, + 'name': 'You saw nothing.', + 'discriminator': 112, + 'in_guild': True + }, + { + 'id': 11, + 'name': 'You saw nothing part 2.', + 'discriminator': 1122, + 'in_guild': False + } + ] + response = self.client.post(url, data=data) self.assertEqual(response.status_code, 400) @@ -213,7 +233,7 @@ class MultiPatchTests(APISubdomainTestCase): } ] response = self.client.patch(url, data=data) - self.assertEqual(response.status_code, 400) + self.assertEqual(response.status_code, 404) def test_returns_400_for_bad_data(self): url = reverse("bot:user-bulk-patch", host="api") @@ -286,3 +306,45 @@ class UserModelTests(APISubdomainTestCase): def test_correct_username_formatting(self): """Tests the username property with both name and discriminator formatted together.""" self.assertEqual(self.user_with_roles.username, "Test User with two roles#0001") + + +class UserPaginatorTests(APISubdomainTestCase): + @classmethod + def setUpTestData(cls): + users = [] + for i in range(1, 10_001): + users.append(User( + id=i, + name=f"user{i}", + discriminator=1111, + in_guild=True + )) + cls.users = User.objects.bulk_create(users) + + def test_returns_single_page_response(self): + url = reverse("bot:user-list", host="api") + response = self.client.get(url).json() + self.assertIsNone(response["next_page_no"]) + self.assertIsNone(response["previous_page_no"]) + + def test_returns_next_page_number(self): + User.objects.create( + id=10_001, + name="user10001", + discriminator=1111, + in_guild=True + ) + url = reverse("bot:user-list", host="api") + response = self.client.get(url).json() + self.assertEqual(2, response["next_page_no"]) + + def test_returns_previous_page_number(self): + User.objects.create( + id=10_001, + name="user10001", + discriminator=1111, + in_guild=True + ) + url = reverse("bot:user-list", host="api") + response = self.client.get(url, {"page": 2}).json() + self.assertEqual(1, response["previous_page_no"]) -- cgit v1.2.3 From c8c6cb8754bd0917e35eb157925028a9b6f1dcb9 Mon Sep 17 00:00:00 2001 From: Lucas Lindstrรถm Date: Tue, 6 Oct 2020 21:29:34 +0200 Subject: Added metricity db connection and user bot API --- docker-compose.yml | 3 +++ postgres/init.sql | 34 +++++++++++++++++++++++ pydis_site/apps/api/viewsets/bot/user.py | 46 ++++++++++++++++++++++++++++++++ pydis_site/settings.py | 3 ++- 4 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 postgres/init.sql diff --git a/docker-compose.yml b/docker-compose.yml index 73d2ff85..7287d8d5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,6 +18,8 @@ services: POSTGRES_DB: pysite POSTGRES_PASSWORD: pysite POSTGRES_USER: pysite + volumes: + - ./postgres/init.sql:/docker-entrypoint-initdb.d/init.sql web: build: @@ -40,6 +42,7 @@ services: - staticfiles:/var/www/static environment: DATABASE_URL: postgres://pysite:pysite@postgres:5432/pysite + METRICITY_DB_URL: postgres://pysite:pysite@postgres:5432/metricity SECRET_KEY: suitable-for-development-only STATIC_ROOT: /var/www/static diff --git a/postgres/init.sql b/postgres/init.sql new file mode 100644 index 00000000..fd29ddbc --- /dev/null +++ b/postgres/init.sql @@ -0,0 +1,34 @@ +CREATE DATABASE metricity; + +\c metricity; + +CREATE TABLE users ( + id varchar(255), + name varchar(255) not null, + avatar_hash varchar(255), + joined_at timestamp not null, + created_at timestamp not null, + is_staff boolean not null, + opt_out boolean default false, + bot boolean default false, + is_guild boolean default true, + is_verified boolean default false, + public_flags text default '{}', + verified_at timestamp, + primary key(id) +); + +INSERT INTO users VALUES ( + 0, + 'foo', + 'bar', + current_timestamp, + current_timestamp, + false, + false, + false, + true, + false, + '{}', + NULL +); diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 9571b3d7..0eeacbb3 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -1,3 +1,10 @@ +import json + +from django.db import connections +from rest_framework import status +from rest_framework.decorators import action +from rest_framework.request import Request +from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet from rest_framework_bulk import BulkCreateModelMixin @@ -53,6 +60,29 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): - 200: returned on success - 404: if a user with the given `snowflake` could not be found + ### GET /bot/users//metricity_data + Gets metricity data for a single user by ID. + + #### Response format + >>> { + ... "id": "0", + ... "name": "foo", + ... "avatar_hash": "bar", + ... "joined_at": "2020-10-06T18:17:30.101677", + ... "created_at": "2020-10-06T18:17:30.101677", + ... "is_staff": False, + ... "opt_out": False, + ... "bot": False, + ... "is_guild": True, + ... "is_verified": False, + ... "public_flags": {}, + ... "verified_at": null + ...} + + #### 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 site. @@ -115,7 +145,23 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): #### Status codes - 204: returned on success - 404: if a user with the given `snowflake` does not exist + + """ serializer_class = UserSerializer queryset = User.objects + + @action(detail=True) + def metricity_data(self, request: Request, pk: str = None) -> Response: + """Request handler for metricity_data endpoint.""" + user = self.get_object() + column_keys = ["id", "name", "avatar_hash", "joined_at", "created_at", "is_staff", + "opt_out", "bot", "is_guild", "is_verified", "public_flags", "verified_at"] + with connections['metricity'].cursor() as cursor: + query = f"SELECT {','.join(column_keys)} FROM users WHERE id = '%s'" + cursor.execute(query, [user.id]) + values = cursor.fetchone() + data = dict(zip(column_keys, values)) + data["public_flags"] = json.loads(data["public_flags"]) + return Response(data, status=status.HTTP_200_OK) diff --git a/pydis_site/settings.py b/pydis_site/settings.py index 3769fa25..2e78e458 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -172,7 +172,8 @@ WSGI_APPLICATION = 'pydis_site.wsgi.application' # https://docs.djangoproject.com/en/2.1/ref/settings/#databases DATABASES = { - 'default': env.db() + 'default': env.db(), + 'metricity': env.db('METRICITY_DB_URL'), } # Password validation -- cgit v1.2.3 From a188ab8ebfa9299addb7fd0effce2de7a0509645 Mon Sep 17 00:00:00 2001 From: Lucas Lindstrรถm Date: Tue, 6 Oct 2020 22:02:08 +0200 Subject: Fix minor style issue. --- pydis_site/apps/api/viewsets/bot/user.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 0eeacbb3..059bc0f0 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -145,8 +145,6 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): #### Status codes - 204: returned on success - 404: if a user with the given `snowflake` does not exist - - """ serializer_class = UserSerializer -- cgit v1.2.3 From 15086b4ab392a8bdc6c33414f0c4e2a294f4a2ef Mon Sep 17 00:00:00 2001 From: Lucas Lindstrรถm Date: Tue, 6 Oct 2020 22:40:14 +0200 Subject: Added total message count to metricity data response. --- postgres/init.sql | 16 ++++++++++++++++ pydis_site/apps/api/viewsets/bot/user.py | 9 ++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/postgres/init.sql b/postgres/init.sql index fd29ddbc..45ad440c 100644 --- a/postgres/init.sql +++ b/postgres/init.sql @@ -32,3 +32,19 @@ INSERT INTO users VALUES ( '{}', NULL ); + +CREATE TABLE messages ( + id varchar(255), + author_id varchar(255) references users(id), + primary key(id) +); + +INSERT INTO messages VALUES( + 0, + 0 +); + +INSERT INTO messages VALUES( + 1, + 0 +); diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 059bc0f0..b3d880cc 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -76,7 +76,8 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): ... "is_guild": True, ... "is_verified": False, ... "public_flags": {}, - ... "verified_at": null + ... "verified_at": None, + ... "total_messages": 2 ...} #### Status codes @@ -157,9 +158,15 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): column_keys = ["id", "name", "avatar_hash", "joined_at", "created_at", "is_staff", "opt_out", "bot", "is_guild", "is_verified", "public_flags", "verified_at"] with connections['metricity'].cursor() as cursor: + # Get user data query = f"SELECT {','.join(column_keys)} FROM users WHERE id = '%s'" cursor.execute(query, [user.id]) values = cursor.fetchone() data = dict(zip(column_keys, values)) data["public_flags"] = json.loads(data["public_flags"]) + + # Get message count + cursor.execute("SELECT COUNT(*) FROM messages WHERE author_id = '%s'", [user.id]) + data["total_messages"], = cursor.fetchone() + return Response(data, status=status.HTTP_200_OK) -- cgit v1.2.3 From e3944dfbeeb829b9e4d4bc69134045a346dbbe01 Mon Sep 17 00:00:00 2001 From: Lucas Lindstrรถm Date: Tue, 6 Oct 2020 23:12:09 +0200 Subject: Add missing metricity db url environment variable to CI pipeline script. --- azure-pipelines.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f273dad3..4f90aafe 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -66,6 +66,7 @@ jobs: env: CI: azure DATABASE_URL: postgres://pysite:pysite@localhost:7777/pysite + METRICITY_DB_URL: postgres://pysite:pysite@localhost:7777/metricity displayName: 'Run Tests' - script: coverage report -m && coverage xml -- cgit v1.2.3 From e83f445a9b8d2db4523e261759bb73ea83ed54c3 Mon Sep 17 00:00:00 2001 From: Lucas Lindstrรถm Date: Tue, 6 Oct 2020 23:56:47 +0200 Subject: Reduce metricity db setup script and API response to the bare necessities. --- postgres/init.sql | 28 ++++------------------------ pydis_site/apps/api/viewsets/bot/user.py | 29 ++++------------------------- 2 files changed, 8 insertions(+), 49 deletions(-) diff --git a/postgres/init.sql b/postgres/init.sql index 45ad440c..922ce1ad 100644 --- a/postgres/init.sql +++ b/postgres/init.sql @@ -3,39 +3,19 @@ CREATE DATABASE metricity; \c metricity; CREATE TABLE users ( - id varchar(255), - name varchar(255) not null, - avatar_hash varchar(255), - joined_at timestamp not null, - created_at timestamp not null, - is_staff boolean not null, - opt_out boolean default false, - bot boolean default false, - is_guild boolean default true, - is_verified boolean default false, - public_flags text default '{}', + id varchar, verified_at timestamp, primary key(id) ); INSERT INTO users VALUES ( 0, - 'foo', - 'bar', - current_timestamp, - current_timestamp, - false, - false, - false, - true, - false, - '{}', - NULL + current_timestamp ); CREATE TABLE messages ( - id varchar(255), - author_id varchar(255) references users(id), + id varchar, + author_id varchar references users(id), primary key(id) ); diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index b3d880cc..1b1af841 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -1,5 +1,3 @@ -import json - from django.db import connections from rest_framework import status from rest_framework.decorators import action @@ -65,18 +63,7 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): #### Response format >>> { - ... "id": "0", - ... "name": "foo", - ... "avatar_hash": "bar", - ... "joined_at": "2020-10-06T18:17:30.101677", - ... "created_at": "2020-10-06T18:17:30.101677", - ... "is_staff": False, - ... "opt_out": False, - ... "bot": False, - ... "is_guild": True, - ... "is_verified": False, - ... "public_flags": {}, - ... "verified_at": None, + ... "verified_at": "2020-10-06T21:54:23.540766", ... "total_messages": 2 ...} @@ -155,18 +142,10 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): def metricity_data(self, request: Request, pk: str = None) -> Response: """Request handler for metricity_data endpoint.""" user = self.get_object() - column_keys = ["id", "name", "avatar_hash", "joined_at", "created_at", "is_staff", - "opt_out", "bot", "is_guild", "is_verified", "public_flags", "verified_at"] with connections['metricity'].cursor() as cursor: - # Get user data - query = f"SELECT {','.join(column_keys)} FROM users WHERE id = '%s'" - cursor.execute(query, [user.id]) - values = cursor.fetchone() - data = dict(zip(column_keys, values)) - data["public_flags"] = json.loads(data["public_flags"]) - - # Get message count + data = {} + cursor.execute("SELECT verified_at FROM users WHERE id = '%s'", [user.id]) + data["verified_at"], = cursor.fetchone() cursor.execute("SELECT COUNT(*) FROM messages WHERE author_id = '%s'", [user.id]) data["total_messages"], = cursor.fetchone() - return Response(data, status=status.HTTP_200_OK) -- cgit v1.2.3 From 7b2275dd0888cc4e91866921ab3be5799eb44986 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Wed, 7 Oct 2020 09:38:45 +0530 Subject: use more efficient code --- pydis_site/apps/api/serializers.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 51afbe73..ed6717e7 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -262,12 +262,14 @@ class UserListSerializer(ListSerializer): def create(self, validated_data: list) -> list: """Override create method to optimize django queries.""" new_users = [] - request_user_ids = [user["id"] for user in validated_data] + seen = set() for user_dict in validated_data: - if request_user_ids.count(user_dict["id"]) > 1: - raise ValidationError({"id": f"User with ID {user_dict['id']} " - f"given multiple times."}) + if user_dict["id"] in seen: + raise ValidationError( + {"id": f"User with ID {user_dict['id']} given multiple times."} + ) + seen.add(user_dict["id"]) new_users.append(User(**user_dict)) return User.objects.bulk_create(new_users, ignore_conflicts=True) -- cgit v1.2.3 From 5d3d305648acce800769c620760b3a642b399276 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Wed, 7 Oct 2020 09:42:28 +0530 Subject: Document changes made to UserListSerializer in UserViewSet --- pydis_site/apps/api/viewsets/bot/user.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index ebea6a45..54723890 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -104,6 +104,8 @@ class UserViewSet(ModelViewSet): ### POST /bot/users Adds a single or multiple new users. The roles attached to the user(s) must be roles known by the site. + User creation process will be skipped if user is already present in the database. + multiple users with the same id in the request data will raise an error. #### Request body >>> { @@ -120,6 +122,7 @@ class UserViewSet(ModelViewSet): #### Status codes - 201: returned on success - 400: if one of the given roles does not exist, or one of the given fields is invalid + - 400: if multiple user objects with the same id is given. ### PUT /bot/users/ Update the user with the given `snowflake`. -- cgit v1.2.3 From 6201c91895b9e7171d63618a74764cfdda4d4fe0 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Wed, 7 Oct 2020 09:55:21 +0530 Subject: add check for insufficient data in update method of UserListSerializer --- pydis_site/apps/api/serializers.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index ed6717e7..98d58e97 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -297,6 +297,11 @@ class UserListSerializer(ListSerializer): updated.append(user) fields_to_update.remove("id") + + if not fields_to_update: + # Raise ValidationError when only id field is given. + raise ValidationError({"data": "Insufficient data provided."}) + User.objects.bulk_update(updated, fields_to_update) return updated -- cgit v1.2.3 From a3bd6568770b54da21147d386f3cadbdd265f69e Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Wed, 7 Oct 2020 09:56:31 +0530 Subject: add test case for insufficient data while bulk updating users. --- pydis_site/apps/api/tests/test_users.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py index fb22c627..52f5d213 100644 --- a/pydis_site/apps/api/tests/test_users.py +++ b/pydis_site/apps/api/tests/test_users.py @@ -251,6 +251,20 @@ class MultiPatchTests(APISubdomainTestCase): response = self.client.patch(url, data=data) self.assertEqual(response.status_code, 400) + def test_returns_400_for_insufficient_data(self): + url = reverse("bot:user-bulk-patch", host="api") + data = [ + { + "id": 1, + + }, + { + "id": 2, + } + ] + response = self.client.patch(url, data=data) + self.assertEqual(response.status_code, 400) + class UserModelTests(APISubdomainTestCase): @classmethod -- cgit v1.2.3 From 5388034d9a00e41f18ba5f476a2c3e347d4bd569 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Wed, 7 Oct 2020 11:24:31 +0530 Subject: update documentation --- pydis_site/apps/api/viewsets/bot/user.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 54723890..740fc439 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -105,7 +105,6 @@ class UserViewSet(ModelViewSet): Adds a single or multiple new users. The roles attached to the user(s) must be roles known by the site. User creation process will be skipped if user is already present in the database. - multiple users with the same id in the request data will raise an error. #### Request body >>> { @@ -122,7 +121,7 @@ class UserViewSet(ModelViewSet): #### Status codes - 201: returned on success - 400: if one of the given roles does not exist, or one of the given fields is invalid - - 400: if multiple user objects with the same id is given. + - 400: if multiple user objects with the same id are given. ### PUT /bot/users/ Update the user with the given `snowflake`. @@ -208,8 +207,9 @@ class UserViewSet(ModelViewSet): ... ] #### Status codes - - 200: Returned on success. + - 200: returned on success. - 400: if the request body was invalid, see response body for details. + - 404: if the user with the given id does not exist. """ queryset = self.get_queryset() try: -- cgit v1.2.3 From 484eba7715fcbcc195d66f5a60ff56c8167ecf0e Mon Sep 17 00:00:00 2001 From: Lucas Lindstrรถm Date: Thu, 8 Oct 2020 00:17:47 +0200 Subject: Broke out metricity connection into an abstraction and added metricity endpoint unit tests. --- .coveragerc | 1 + pydis_site/apps/api/models/bot/metricity.py | 42 +++++++++++++++++++++ pydis_site/apps/api/tests/test_users.py | 58 +++++++++++++++++++++++++++++ pydis_site/apps/api/viewsets/bot/user.py | 17 +++++---- 4 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 pydis_site/apps/api/models/bot/metricity.py diff --git a/.coveragerc b/.coveragerc index f5ddf08d..0cccc47c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -12,6 +12,7 @@ omit = */admin.py */apps.py */urls.py + pydis_site/apps/api/models/bot/metricity.py pydis_site/wsgi.py pydis_site/settings.py pydis_site/utils/resources.py diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py new file mode 100644 index 00000000..25b42fa2 --- /dev/null +++ b/pydis_site/apps/api/models/bot/metricity.py @@ -0,0 +1,42 @@ +from django.db import connections + + +class NotFound(Exception): + """Raised when an entity cannot be found.""" + + pass + + +class Metricity: + """Abstraction for a connection to the metricity database.""" + + def __init__(self): + self.cursor = connections['metricity'].cursor() + + def __enter__(self): + return self + + def __exit__(self, *_): + self.cursor.close() + + def user(self, user_id: str) -> dict: + """Query a user's data.""" + columns = ["verified_at"] + query = f"SELECT {','.join(columns)} FROM users WHERE id = '%s'" + self.cursor.execute(query, [user_id]) + values = self.cursor.fetchone() + + if not values: + raise NotFound() + + return dict(zip(columns, values)) + + def total_messages(self, user_id: str) -> int: + """Query total number of messages for a user.""" + self.cursor.execute("SELECT COUNT(*) FROM messages WHERE author_id = '%s'", [user_id]) + values = self.cursor.fetchone() + + if not values: + raise NotFound() + + return values[0] diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py index a02fce8a..76a21d3a 100644 --- a/pydis_site/apps/api/tests/test_users.py +++ b/pydis_site/apps/api/tests/test_users.py @@ -1,7 +1,10 @@ +from unittest.mock import patch + from django_hosts.resolvers import reverse from .base import APISubdomainTestCase from ..models import Role, User +from ..models.bot.metricity import NotFound class UnauthedUserAPITests(APISubdomainTestCase): @@ -170,3 +173,58 @@ class UserModelTests(APISubdomainTestCase): def test_correct_username_formatting(self): """Tests the username property with both name and discriminator formatted together.""" self.assertEqual(self.user_with_roles.username, "Test User with two roles#0001") + + +class UserMetricityTests(APISubdomainTestCase): + @classmethod + def setUpTestData(cls): + User.objects.create( + id=0, + name="Test user", + discriminator=1, + in_guild=True, + ) + + def test_get_metricity_data(self): + # Given + verified_at = "foo" + total_messages = 1 + self.mock_metricity_user(verified_at, total_messages) + + # When + url = reverse('bot:user-metricity-data', args=[0], host='api') + response = self.client.get(url) + + # Then + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), { + "verified_at": verified_at, + "total_messages": total_messages, + }) + + def test_no_metricity_user(self): + # Given + self.mock_no_metricity_user() + + # When + url = reverse('bot:user-metricity-data', args=[0], host='api') + response = self.client.get(url) + + # Then + self.assertEqual(response.status_code, 404) + + def mock_metricity_user(self, verified_at, total_messages): + patcher = patch("pydis_site.apps.api.viewsets.bot.user.Metricity") + self.metricity = patcher.start() + self.addCleanup(patcher.stop) + self.metricity = self.metricity.return_value.__enter__.return_value + self.metricity.user.return_value = dict(verified_at=verified_at) + self.metricity.total_messages.return_value = total_messages + + def mock_no_metricity_user(self): + patcher = patch("pydis_site.apps.api.viewsets.bot.user.Metricity") + self.metricity = patcher.start() + self.addCleanup(patcher.stop) + self.metricity = self.metricity.return_value.__enter__.return_value + self.metricity.user.side_effect = NotFound() + self.metricity.total_messages.side_effect = NotFound() diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 1b1af841..352d77c0 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -1,4 +1,3 @@ -from django.db import connections from rest_framework import status from rest_framework.decorators import action from rest_framework.request import Request @@ -6,6 +5,7 @@ from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet from rest_framework_bulk import BulkCreateModelMixin +from pydis_site.apps.api.models.bot.metricity import Metricity, NotFound from pydis_site.apps.api.models.bot.user import User from pydis_site.apps.api.serializers import UserSerializer @@ -142,10 +142,11 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): def metricity_data(self, request: Request, pk: str = None) -> Response: """Request handler for metricity_data endpoint.""" user = self.get_object() - with connections['metricity'].cursor() as cursor: - data = {} - cursor.execute("SELECT verified_at FROM users WHERE id = '%s'", [user.id]) - data["verified_at"], = cursor.fetchone() - cursor.execute("SELECT COUNT(*) FROM messages WHERE author_id = '%s'", [user.id]) - data["total_messages"], = cursor.fetchone() - return Response(data, status=status.HTTP_200_OK) + with Metricity() as metricity: + try: + data = metricity.user(user.id) + data["total_messages"] = metricity.total_messages(user.id) + return Response(data, status=status.HTTP_200_OK) + except NotFound: + return Response(dict(detail="User not found in metricity"), + status=status.HTTP_404_NOT_FOUND) -- cgit v1.2.3 From 995c62b509242d5ae75f8c7a19840ff0f1832d44 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Thu, 8 Oct 2020 12:24:05 +0530 Subject: catch IntegrityError and raise ValidationError during user creation. --- pydis_site/apps/api/serializers.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 98d58e97..a36bf72f 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -1,5 +1,6 @@ """Converters from Django models to data interchange formats and back.""" from django.db.models.query import QuerySet +from django.db.utils import IntegrityError from rest_framework.exceptions import NotFound from rest_framework.serializers import ( IntegerField, @@ -320,6 +321,13 @@ class UserSerializer(ModelSerializer): depth = 1 list_serializer_class = UserListSerializer + def create(self, validated_data: dict) -> User: + """Override create method to catch IntegrityError.""" + try: + return super().create(validated_data) + except IntegrityError: + raise ValidationError({"ID": "User with ID already present."}) + class NominationSerializer(ModelSerializer): """A class providing (de-)serialization of `Nomination` instances.""" -- cgit v1.2.3 From 69693337cfe89df6ae59c50e9076b6d935e24a50 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Thu, 8 Oct 2020 12:30:51 +0530 Subject: fix bug: bulk_patch returns duplicate objects in the response --- pydis_site/apps/api/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index a36bf72f..a4410566 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -295,7 +295,7 @@ class UserListSerializer(ListSerializer): raise NotFound({"id": f"User with id {user_data['id']} not found."}) user.__dict__.update(user_data) - updated.append(user) + updated.append(user) fields_to_update.remove("id") -- cgit v1.2.3 From a9074045245d5c301cabc01c79785a2a3f846682 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Thu, 8 Oct 2020 12:57:40 +0530 Subject: add test case: test_returns_400_for_existing_user --- pydis_site/apps/api/tests/test_users.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py index 52f5d213..1569e4d9 100644 --- a/pydis_site/apps/api/tests/test_users.py +++ b/pydis_site/apps/api/tests/test_users.py @@ -154,6 +154,18 @@ class CreationTests(APISubdomainTestCase): response = self.client.post(url, data=data) self.assertEqual(response.status_code, 400) + def test_returns_400_for_existing_user(self): + """Returns 400 if user is already present in DB.""" + url = reverse('bot:user-list', host='api') + data = { + 'id': 11, + 'name': 'You saw nothing part 3.', + 'discriminator': 1122, + 'in_guild': True + } + response = self.client.post(url, data=data) + self.assertEqual(response.status_code, 400) + class MultiPatchTests(APISubdomainTestCase): @classmethod @@ -256,7 +268,6 @@ class MultiPatchTests(APISubdomainTestCase): data = [ { "id": 1, - }, { "id": 2, -- cgit v1.2.3 From cb32322b8bacabecccdf896d0f7db0355177ac72 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Thu, 8 Oct 2020 13:50:50 +0530 Subject: raise ValidationError if users have same ID in request data during bulk patch --- pydis_site/apps/api/viewsets/bot/user.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 740fc439..46e682d8 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -6,7 +6,7 @@ from rest_framework.decorators import action from rest_framework.pagination import PageNumberPagination from rest_framework.request import Request from rest_framework.response import Response -from rest_framework.serializers import ModelSerializer +from rest_framework.serializers import ModelSerializer, ValidationError from rest_framework.viewsets import ModelViewSet from pydis_site.apps.api.models.bot.user import User @@ -212,14 +212,20 @@ class UserViewSet(ModelViewSet): - 404: if the user with the given id does not exist. """ queryset = self.get_queryset() - try: - object_ids = [item["id"] for item in request.data] - except KeyError: - # user ID not provided in request body. - resp = { - "Error": "User ID not provided." - } - return Response(resp, status=status.HTTP_400_BAD_REQUEST) + object_ids = set() + for data in request.data: + try: + if data["id"] in object_ids: + # If request data contains users with same ID. + raise ValidationError( + {"id": [f"User with ID {data['id']} given multiple times."]} + ) + except KeyError: + # If user ID not provided in request body. + raise ValidationError( + {"id": ["This field is required."]} + ) + object_ids.add(data["id"]) filtered_instances = queryset.filter(id__in=object_ids) -- cgit v1.2.3 From 446db59473706c1c44a9a81a6470a87988a99cfb Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Thu, 8 Oct 2020 13:51:44 +0530 Subject: add testcase: test_returns_400_for_duplicate_request_users --- pydis_site/apps/api/tests/test_users.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py index 1569e4d9..8ed56e83 100644 --- a/pydis_site/apps/api/tests/test_users.py +++ b/pydis_site/apps/api/tests/test_users.py @@ -276,6 +276,22 @@ class MultiPatchTests(APISubdomainTestCase): response = self.client.patch(url, data=data) self.assertEqual(response.status_code, 400) + def test_returns_400_for_duplicate_request_users(self): + """Return 400 if 2 Users with same ID is passed in the request data.""" + url = reverse("bot:user-bulk-patch", host="api") + data = [ + { + 'id': 1, + 'name': 'You saw nothing.', + }, + { + 'id': 1, + 'name': 'You saw nothing part 2.', + } + ] + response = self.client.patch(url, data=data) + self.assertEqual(response.status_code, 400) + class UserModelTests(APISubdomainTestCase): @classmethod -- cgit v1.2.3 From ffab239d674ada91fc4d6c265f6b1aa1c82b9c2c Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Thu, 8 Oct 2020 14:12:19 +0530 Subject: update documentation --- pydis_site/apps/api/viewsets/bot/user.py | 70 ++++++++++++++++---------------- 1 file changed, 34 insertions(+), 36 deletions(-) diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 46e682d8..77142c30 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -73,9 +73,9 @@ class UserViewSet(ModelViewSet): ... ] ... } - #### Query Parameters - - page_size: Number of Users in one page. - - page: Page number + #### Optional Query Parameters + - page_size: number of Users in one page, defaults to 10,000 + - page: page number #### Status codes - 200: returned on success @@ -104,7 +104,7 @@ class UserViewSet(ModelViewSet): ### POST /bot/users Adds a single or multiple new users. The roles attached to the user(s) must be roles known by the site. - User creation process will be skipped if user is already present in the database. + Users that already exist in the database will be skipped. #### Request body >>> { @@ -121,7 +121,7 @@ class UserViewSet(ModelViewSet): #### Status codes - 201: returned on success - 400: if one of the given roles does not exist, or one of the given fields is invalid - - 400: if multiple user objects with the same id are given. + - 400: if multiple user objects with the same id are given ### PUT /bot/users/ Update the user with the given `snowflake`. @@ -159,6 +159,34 @@ class UserViewSet(ModelViewSet): - 400: if the request body was invalid, see response body for details - 404: if the user with the given `snowflake` could not be found + ### BULK PATCH /bot/users/bulk_patch + Update users with the given `ids` and `details`. + `id` field and at least one other field is mandatory. + + #### Request body + >>> [ + ... { + ... 'id': int, + ... 'name': str, + ... 'discriminator': int, + ... 'roles': List[int], + ... 'in_guild': bool + ... }, + ... { + ... 'id': int, + ... '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 + - 400: if multiple user objects with the same id are given + - 404: if the user with the given id does not exist + ### DELETE /bot/users/ Deletes the user with the given `snowflake`. @@ -180,37 +208,7 @@ class UserViewSet(ModelViewSet): @action(detail=False, methods=["PATCH"], name='user-bulk-patch') def bulk_patch(self, request: Request) -> Response: - """ - Update multiple User objects in a single request. - - ## Route - ### PATCH /bot/users/bulk_patch - Update all users with the IDs. - `id` field is mandatory, rest are optional. - - #### Request body - >>> [ - ... { - ... 'id': int, - ... 'name': str, - ... 'discriminator': int, - ... 'roles': List[int], - ... 'in_guild': bool - ... }, - ... { - ... 'id': int, - ... '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 id does not exist. - """ + """Update multiple User objects in a single request.""" queryset = self.get_queryset() object_ids = set() for data in request.data: -- cgit v1.2.3 From fe851bd091a32196b4171b289fc6dba5c2bf65a8 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Thu, 8 Oct 2020 14:26:05 +0530 Subject: use NON_FIELD_ERRORS_KEY for non-field-specific ValidationError response --- pydis_site/apps/api/serializers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index a4410566..74d1ac8c 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -9,6 +9,7 @@ from rest_framework.serializers import ( PrimaryKeyRelatedField, ValidationError ) +from rest_framework.settings import api_settings from rest_framework.validators import UniqueTogetherValidator from .models import ( @@ -301,7 +302,9 @@ class UserListSerializer(ListSerializer): if not fields_to_update: # Raise ValidationError when only id field is given. - raise ValidationError({"data": "Insufficient data provided."}) + raise ValidationError( + {api_settings.NON_FIELD_ERRORS_KEY: ["Insufficient data provided."]} + ) User.objects.bulk_update(updated, fields_to_update) return updated -- cgit v1.2.3 From cd72f72373a0b317cee6c99cf5f05ad8dfeb77d1 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Thu, 8 Oct 2020 14:31:50 +0530 Subject: normalize API error responses. --- pydis_site/apps/api/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 74d1ac8c..155f33f2 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -293,7 +293,7 @@ class UserListSerializer(ListSerializer): try: user = instance_mapping[user_data["id"]] except KeyError: - raise NotFound({"id": f"User with id {user_data['id']} not found."}) + raise NotFound({"detail": [f"User with id {user_data['id']} not found."]}) user.__dict__.update(user_data) updated.append(user) @@ -329,7 +329,7 @@ class UserSerializer(ModelSerializer): try: return super().create(validated_data) except IntegrityError: - raise ValidationError({"ID": "User with ID already present."}) + raise ValidationError({"id": ["User with ID already present."]}) class NominationSerializer(ModelSerializer): -- cgit v1.2.3 From e7413978556567d86c92ff643b6d2173d09f44b7 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Thu, 8 Oct 2020 14:32:28 +0530 Subject: correct indentation --- pydis_site/apps/api/viewsets/bot/user.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 77142c30..22e61915 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -166,18 +166,18 @@ class UserViewSet(ModelViewSet): #### Request body >>> [ ... { - ... 'id': int, - ... 'name': str, - ... 'discriminator': int, - ... 'roles': List[int], - ... 'in_guild': bool + ... 'id': int, + ... 'name': str, + ... 'discriminator': int, + ... 'roles': List[int], + ... 'in_guild': bool ... }, ... { - ... 'id': int, - ... 'name': str, - ... 'discriminator': int, - ... 'roles': List[int], - ... 'in_guild': bool + ... 'id': int, + ... 'name': str, + ... 'discriminator': int, + ... 'roles': List[int], + ... 'in_guild': bool ... }, ... ] -- cgit v1.2.3 From bdc69a74e27a65fb9b8ce67a878c6a953f6777b8 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Thu, 8 Oct 2020 14:57:50 +0530 Subject: fix bug with bulk create: response includes objects for users which were duplicates --- pydis_site/apps/api/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 155f33f2..4f56e52f 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -274,7 +274,8 @@ class UserListSerializer(ListSerializer): seen.add(user_dict["id"]) new_users.append(User(**user_dict)) - return User.objects.bulk_create(new_users, ignore_conflicts=True) + users = User.objects.bulk_create(new_users, ignore_conflicts=True) + return User.objects.filter(id__in=[user.id for user in users]) def update(self, instance: QuerySet, validated_data: list) -> list: """ -- cgit v1.2.3 From 570f0cf226ebabbd4f41a19ded90f70d0ec96f9a Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Thu, 8 Oct 2020 22:49:57 +0530 Subject: return empty list as response for bulk creation of users --- pydis_site/apps/api/serializers.py | 4 ++-- pydis_site/apps/api/tests/test_users.py | 2 +- pydis_site/apps/api/viewsets/bot/user.py | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index efd4f7ce..31293c86 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -260,8 +260,8 @@ class UserListSerializer(ListSerializer): seen.add(user_dict["id"]) new_users.append(User(**user_dict)) - users = User.objects.bulk_create(new_users, ignore_conflicts=True) - return User.objects.filter(id__in=[user.id for user in users]) + User.objects.bulk_create(new_users, ignore_conflicts=True) + return [] def update(self, instance: QuerySet, validated_data: list) -> list: """ diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py index 8ed56e83..825e4edb 100644 --- a/pydis_site/apps/api/tests/test_users.py +++ b/pydis_site/apps/api/tests/test_users.py @@ -96,7 +96,7 @@ class CreationTests(APISubdomainTestCase): response = self.client.post(url, data=data) self.assertEqual(response.status_code, 201) - self.assertEqual(response.json(), data) + self.assertEqual(response.json(), []) def test_returns_400_for_unknown_role_id(self): url = reverse('bot:user-list', host='api') diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 22e61915..5c509e50 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -116,7 +116,8 @@ class UserViewSet(ModelViewSet): ... } Alternatively, request users can be POSTed as a list of above objects, - in which case multiple users will be created at once. + in which case multiple users will be created at once. In this case, + the response is an empty list. #### Status codes - 201: returned on success -- cgit v1.2.3 From e405b60de8f46ad1ef0ec8b2f0ea8cdd12ef2e55 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Thu, 8 Oct 2020 23:02:42 +0530 Subject: supress warning: UnorderedObjectListWarning: Pagination may yield inconsistent results with an unordered object_list: QuerySet. --- pydis_site/apps/api/viewsets/bot/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 5c509e50..9ddd13d4 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -197,7 +197,7 @@ class UserViewSet(ModelViewSet): """ serializer_class = UserSerializer - queryset = User.objects.all() + queryset = User.objects.all().order_by("id") pagination_class = UserListPagination def get_serializer(self, *args, **kwargs) -> ModelSerializer: -- cgit v1.2.3 From a57035b893e223c35d73d0994343d3c407b157fb Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Thu, 8 Oct 2020 23:22:18 +0530 Subject: Convert error message value from list to string --- pydis_site/apps/api/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 31293c86..ac155a78 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -280,7 +280,7 @@ class UserListSerializer(ListSerializer): try: user = instance_mapping[user_data["id"]] except KeyError: - raise NotFound({"detail": [f"User with id {user_data['id']} not found."]}) + raise NotFound({"detail": f"User with id {user_data['id']} not found."}) user.__dict__.update(user_data) updated.append(user) -- cgit v1.2.3 From 4e43549591f3c027f47de45c5a4a524236af3bb4 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Thu, 8 Oct 2020 23:40:41 +0530 Subject: Convert ValidationError response value to list from string --- pydis_site/apps/api/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index ac155a78..ee950cf4 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -255,7 +255,7 @@ class UserListSerializer(ListSerializer): for user_dict in validated_data: if user_dict["id"] in seen: raise ValidationError( - {"id": f"User with ID {user_dict['id']} given multiple times."} + {"id": [f"User with ID {user_dict['id']} given multiple times."]} ) seen.add(user_dict["id"]) new_users.append(User(**user_dict)) -- cgit v1.2.3 From 1bef883886f3335a441b496b35df9e25b3ba0e91 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Fri, 9 Oct 2020 00:02:24 +0530 Subject: Move Validation checks to serializer from viewset --- pydis_site/apps/api/serializers.py | 23 +++++++++++++++++++++-- pydis_site/apps/api/viewsets/bot/user.py | 22 ++-------------------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index ee950cf4..25c5c82e 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -263,13 +263,32 @@ class UserListSerializer(ListSerializer): User.objects.bulk_create(new_users, ignore_conflicts=True) return [] - def update(self, instance: QuerySet, validated_data: list) -> list: + def update(self, queryset: QuerySet, validated_data: list) -> list: """ Override update method to support bulk updates. ref:https://www.django-rest-framework.org/api-guide/serializers/#customizing-multiple-update """ - instance_mapping = {user.id: user for user in instance} + object_ids = set() + + for data in validated_data: + try: + if data["id"] in object_ids: + # If request data contains users with same ID. + raise ValidationError( + {"id": [f"User with ID {data['id']} given multiple times."]} + ) + except KeyError: + # If user ID not provided in request body. + raise ValidationError( + {"id": ["This field is required."]} + ) + object_ids.add(data["id"]) + + # filter queryset + filtered_instances = queryset.filter(id__in=object_ids) + + instance_mapping = {user.id: user for user in filtered_instances} updated = [] fields_to_update = set() diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 9ddd13d4..3e4b627e 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -6,7 +6,7 @@ from rest_framework.decorators import action from rest_framework.pagination import PageNumberPagination from rest_framework.request import Request from rest_framework.response import Response -from rest_framework.serializers import ModelSerializer, ValidationError +from rest_framework.serializers import ModelSerializer from rest_framework.viewsets import ModelViewSet from pydis_site.apps.api.models.bot.user import User @@ -210,26 +210,8 @@ class UserViewSet(ModelViewSet): @action(detail=False, methods=["PATCH"], name='user-bulk-patch') def bulk_patch(self, request: Request) -> Response: """Update multiple User objects in a single request.""" - queryset = self.get_queryset() - object_ids = set() - for data in request.data: - try: - if data["id"] in object_ids: - # If request data contains users with same ID. - raise ValidationError( - {"id": [f"User with ID {data['id']} given multiple times."]} - ) - except KeyError: - # If user ID not provided in request body. - raise ValidationError( - {"id": ["This field is required."]} - ) - object_ids.add(data["id"]) - - filtered_instances = queryset.filter(id__in=object_ids) - serializer = self.get_serializer( - instance=filtered_instances, + instance=self.get_queryset(), data=request.data, many=True, partial=True -- cgit v1.2.3 From 4006c73b7af293e96f9902c08943df59bbe28228 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sat, 10 Oct 2020 13:11:01 +0100 Subject: Update page title --- pydis_site/templates/home/timeline.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/templates/home/timeline.html b/pydis_site/templates/home/timeline.html index d9cdd625..b7f7cda9 100644 --- a/pydis_site/templates/home/timeline.html +++ b/pydis_site/templates/home/timeline.html @@ -1,7 +1,7 @@ {% extends 'base/base.html' %} {% load static %} -{% block title %}Home{% endblock %} +{% block title %}Timeline{% endblock %} {% block head %} -- cgit v1.2.3 From ec488e6aa5d156521b34d1363aac9fd568a3e7be Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sat, 10 Oct 2020 13:23:24 +0100 Subject: Prettify CSS --- pydis_site/static/css/home/style.css | 3731 +++++++++++++++++++++++++++++++++- 1 file changed, 3730 insertions(+), 1 deletion(-) diff --git a/pydis_site/static/css/home/style.css b/pydis_site/static/css/home/style.css index 4eaca7c2..7085bf5a 100644 --- a/pydis_site/static/css/home/style.css +++ b/pydis_site/static/css/home/style.css @@ -1 +1,3730 @@ -@import url("https://fonts.googleapis.com/css?family=Droid+Serif|Open+Sans:400,700");*,*::after,*::before{box-sizing:inherit}*{font:inherit}html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video,hr{margin:0;padding:0;border:0}html{box-sizing:border-box}body{background-color:hsl(0, 0%, 100%);background-color:var(--color-bg, white)}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section,main,form legend{display:block}ol,ul{list-style:none}blockquote,q{quotes:none}button,input,textarea,select{margin:0}.btn,.form-control,.link,.reset{background-color:transparent;padding:0;border:0;border-radius:0;color:inherit;line-height:inherit;-webkit-appearance:none;-moz-appearance:none;appearance:none}select.form-control::-ms-expand{display:none}textarea{resize:vertical;overflow:auto;vertical-align:top}input::-ms-clear{display:none}table{border-collapse:collapse;border-spacing:0}img,video,svg{max-width:100%}[data-theme]{background-color:hsl(0, 0%, 100%);background-color:var(--color-bg, #fff);color:hsl(240, 4%, 20%);color:var(--color-contrast-high, #313135)}:root{--space-unit: 1em;--space-xxxxs: calc(0.125*var(--space-unit));--space-xxxs: calc(0.25*var(--space-unit));--space-xxs: calc(0.375*var(--space-unit));--space-xs: calc(0.5*var(--space-unit));--space-sm: calc(0.75*var(--space-unit));--space-md: calc(1.25*var(--space-unit));--space-lg: calc(2*var(--space-unit));--space-xl: calc(3.25*var(--space-unit));--space-xxl: calc(5.25*var(--space-unit));--space-xxxl: calc(8.5*var(--space-unit));--space-xxxxl: calc(13.75*var(--space-unit));--component-padding: var(--space-md)}:root{--max-width-xxs: 32rem;--max-width-xs: 38rem;--max-width-sm: 48rem;--max-width-md: 64rem;--max-width-lg: 80rem;--max-width-xl: 90rem;--max-width-xxl: 120rem}.container{width:calc(100% - 1.25em);width:calc(100% - 2*var(--component-padding));margin-left:auto;margin-right:auto}.max-width-xxs{max-width:32rem;max-width:var(--max-width-xxs)}.max-width-xs{max-width:38rem;max-width:var(--max-width-xs)}.max-width-sm{max-width:48rem;max-width:var(--max-width-sm)}.max-width-md{max-width:64rem;max-width:var(--max-width-md)}.max-width-lg{max-width:80rem;max-width:var(--max-width-lg)}.max-width-xl{max-width:90rem;max-width:var(--max-width-xl)}.max-width-xxl{max-width:120rem;max-width:var(--max-width-xxl)}.max-width-adaptive-sm{max-width:38rem;max-width:var(--max-width-xs)}@media (min-width: 64rem){.max-width-adaptive-sm{max-width:48rem;max-width:var(--max-width-sm)}}.max-width-adaptive-md{max-width:38rem;max-width:var(--max-width-xs)}@media (min-width: 64rem){.max-width-adaptive-md{max-width:64rem;max-width:var(--max-width-md)}}.max-width-adaptive,.max-width-adaptive-lg{max-width:38rem;max-width:var(--max-width-xs)}@media (min-width: 64rem){.max-width-adaptive,.max-width-adaptive-lg{max-width:64rem;max-width:var(--max-width-md)}}@media (min-width: 90rem){.max-width-adaptive,.max-width-adaptive-lg{max-width:80rem;max-width:var(--max-width-lg)}}.max-width-adaptive-xl{max-width:38rem;max-width:var(--max-width-xs)}@media (min-width: 64rem){.max-width-adaptive-xl{max-width:64rem;max-width:var(--max-width-md)}}@media (min-width: 90rem){.max-width-adaptive-xl{max-width:90rem;max-width:var(--max-width-xl)}}.grid{--grid-gap: 0px;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.grid>*{-ms-flex-preferred-size:100%;flex-basis:100%}[class*="grid-gap"]{margin-bottom:1em * -1;margin-bottom:calc(var(--grid-gap, 1em)*-1);margin-right:1em * -1;margin-right:calc(var(--grid-gap, 1em)*-1)}[class*="grid-gap"]>*{margin-bottom:1em;margin-bottom:var(--grid-gap, 1em);margin-right:1em;margin-right:var(--grid-gap, 1em)}.grid-gap-xxxxs{--grid-gap: var(--space-xxxxs)}.grid-gap-xxxs{--grid-gap: var(--space-xxxs)}.grid-gap-xxs{--grid-gap: var(--space-xxs)}.grid-gap-xs{--grid-gap: var(--space-xs)}.grid-gap-sm{--grid-gap: var(--space-sm)}.grid-gap-md{--grid-gap: var(--space-md)}.grid-gap-lg{--grid-gap: var(--space-lg)}.grid-gap-xl{--grid-gap: var(--space-xl)}.grid-gap-xxl{--grid-gap: var(--space-xxl)}.grid-gap-xxxl{--grid-gap: var(--space-xxxl)}.grid-gap-xxxxl{--grid-gap: var(--space-xxxxl)}.col{-ms-flex-positive:1;flex-grow:1;-ms-flex-preferred-size:0;flex-basis:0;max-width:100%}.col-1{-ms-flex-preferred-size:calc(8.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(8.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(8.33% - 0.01px - 1em);flex-basis:calc(8.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(8.33% - 0.01px - 1em);max-width:calc(8.33% - 0.01px - var(--grid-gap, 1em))}.col-2{-ms-flex-preferred-size:calc(16.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(16.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(16.66% - 0.01px - 1em);flex-basis:calc(16.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(16.66% - 0.01px - 1em);max-width:calc(16.66% - 0.01px - var(--grid-gap, 1em))}.col-3{-ms-flex-preferred-size:calc(25% - 0.01px - 1em);-ms-flex-preferred-size:calc(25% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(25% - 0.01px - 1em);flex-basis:calc(25% - 0.01px - var(--grid-gap, 1em));max-width:calc(25% - 0.01px - 1em);max-width:calc(25% - 0.01px - var(--grid-gap, 1em))}.col-4{-ms-flex-preferred-size:calc(33.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(33.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(33.33% - 0.01px - 1em);flex-basis:calc(33.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(33.33% - 0.01px - 1em);max-width:calc(33.33% - 0.01px - var(--grid-gap, 1em))}.col-5{-ms-flex-preferred-size:calc(41.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(41.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(41.66% - 0.01px - 1em);flex-basis:calc(41.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(41.66% - 0.01px - 1em);max-width:calc(41.66% - 0.01px - var(--grid-gap, 1em))}.col-6{-ms-flex-preferred-size:calc(50% - 0.01px - 1em);-ms-flex-preferred-size:calc(50% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(50% - 0.01px - 1em);flex-basis:calc(50% - 0.01px - var(--grid-gap, 1em));max-width:calc(50% - 0.01px - 1em);max-width:calc(50% - 0.01px - var(--grid-gap, 1em))}.col-7{-ms-flex-preferred-size:calc(58.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(58.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(58.33% - 0.01px - 1em);flex-basis:calc(58.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(58.33% - 0.01px - 1em);max-width:calc(58.33% - 0.01px - var(--grid-gap, 1em))}.col-8{-ms-flex-preferred-size:calc(66.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(66.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(66.66% - 0.01px - 1em);flex-basis:calc(66.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(66.66% - 0.01px - 1em);max-width:calc(66.66% - 0.01px - var(--grid-gap, 1em))}.col-9{-ms-flex-preferred-size:calc(75% - 0.01px - 1em);-ms-flex-preferred-size:calc(75% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(75% - 0.01px - 1em);flex-basis:calc(75% - 0.01px - var(--grid-gap, 1em));max-width:calc(75% - 0.01px - 1em);max-width:calc(75% - 0.01px - var(--grid-gap, 1em))}.col-10{-ms-flex-preferred-size:calc(83.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(83.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(83.33% - 0.01px - 1em);flex-basis:calc(83.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(83.33% - 0.01px - 1em);max-width:calc(83.33% - 0.01px - var(--grid-gap, 1em))}.col-11{-ms-flex-preferred-size:calc(91.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(91.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(91.66% - 0.01px - 1em);flex-basis:calc(91.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(91.66% - 0.01px - 1em);max-width:calc(91.66% - 0.01px - var(--grid-gap, 1em))}.col-12{-ms-flex-preferred-size:calc(100% - 0.01px - 1em);-ms-flex-preferred-size:calc(100% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(100% - 0.01px - 1em);flex-basis:calc(100% - 0.01px - var(--grid-gap, 1em));max-width:calc(100% - 0.01px - 1em);max-width:calc(100% - 0.01px - var(--grid-gap, 1em))}@media (min-width: 32rem){.col\@xs{-ms-flex-positive:1;flex-grow:1;-ms-flex-preferred-size:0;flex-basis:0;max-width:100%}.col-1\@xs{-ms-flex-preferred-size:calc(8.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(8.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(8.33% - 0.01px - 1em);flex-basis:calc(8.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(8.33% - 0.01px - 1em);max-width:calc(8.33% - 0.01px - var(--grid-gap, 1em))}.col-2\@xs{-ms-flex-preferred-size:calc(16.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(16.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(16.66% - 0.01px - 1em);flex-basis:calc(16.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(16.66% - 0.01px - 1em);max-width:calc(16.66% - 0.01px - var(--grid-gap, 1em))}.col-3\@xs{-ms-flex-preferred-size:calc(25% - 0.01px - 1em);-ms-flex-preferred-size:calc(25% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(25% - 0.01px - 1em);flex-basis:calc(25% - 0.01px - var(--grid-gap, 1em));max-width:calc(25% - 0.01px - 1em);max-width:calc(25% - 0.01px - var(--grid-gap, 1em))}.col-4\@xs{-ms-flex-preferred-size:calc(33.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(33.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(33.33% - 0.01px - 1em);flex-basis:calc(33.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(33.33% - 0.01px - 1em);max-width:calc(33.33% - 0.01px - var(--grid-gap, 1em))}.col-5\@xs{-ms-flex-preferred-size:calc(41.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(41.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(41.66% - 0.01px - 1em);flex-basis:calc(41.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(41.66% - 0.01px - 1em);max-width:calc(41.66% - 0.01px - var(--grid-gap, 1em))}.col-6\@xs{-ms-flex-preferred-size:calc(50% - 0.01px - 1em);-ms-flex-preferred-size:calc(50% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(50% - 0.01px - 1em);flex-basis:calc(50% - 0.01px - var(--grid-gap, 1em));max-width:calc(50% - 0.01px - 1em);max-width:calc(50% - 0.01px - var(--grid-gap, 1em))}.col-7\@xs{-ms-flex-preferred-size:calc(58.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(58.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(58.33% - 0.01px - 1em);flex-basis:calc(58.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(58.33% - 0.01px - 1em);max-width:calc(58.33% - 0.01px - var(--grid-gap, 1em))}.col-8\@xs{-ms-flex-preferred-size:calc(66.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(66.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(66.66% - 0.01px - 1em);flex-basis:calc(66.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(66.66% - 0.01px - 1em);max-width:calc(66.66% - 0.01px - var(--grid-gap, 1em))}.col-9\@xs{-ms-flex-preferred-size:calc(75% - 0.01px - 1em);-ms-flex-preferred-size:calc(75% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(75% - 0.01px - 1em);flex-basis:calc(75% - 0.01px - var(--grid-gap, 1em));max-width:calc(75% - 0.01px - 1em);max-width:calc(75% - 0.01px - var(--grid-gap, 1em))}.col-10\@xs{-ms-flex-preferred-size:calc(83.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(83.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(83.33% - 0.01px - 1em);flex-basis:calc(83.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(83.33% - 0.01px - 1em);max-width:calc(83.33% - 0.01px - var(--grid-gap, 1em))}.col-11\@xs{-ms-flex-preferred-size:calc(91.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(91.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(91.66% - 0.01px - 1em);flex-basis:calc(91.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(91.66% - 0.01px - 1em);max-width:calc(91.66% - 0.01px - var(--grid-gap, 1em))}.col-12\@xs{-ms-flex-preferred-size:calc(100% - 0.01px - 1em);-ms-flex-preferred-size:calc(100% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(100% - 0.01px - 1em);flex-basis:calc(100% - 0.01px - var(--grid-gap, 1em));max-width:calc(100% - 0.01px - 1em);max-width:calc(100% - 0.01px - var(--grid-gap, 1em))}}@media (min-width: 48rem){.col\@sm{-ms-flex-positive:1;flex-grow:1;-ms-flex-preferred-size:0;flex-basis:0;max-width:100%}.col-1\@sm{-ms-flex-preferred-size:calc(8.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(8.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(8.33% - 0.01px - 1em);flex-basis:calc(8.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(8.33% - 0.01px - 1em);max-width:calc(8.33% - 0.01px - var(--grid-gap, 1em))}.col-2\@sm{-ms-flex-preferred-size:calc(16.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(16.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(16.66% - 0.01px - 1em);flex-basis:calc(16.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(16.66% - 0.01px - 1em);max-width:calc(16.66% - 0.01px - var(--grid-gap, 1em))}.col-3\@sm{-ms-flex-preferred-size:calc(25% - 0.01px - 1em);-ms-flex-preferred-size:calc(25% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(25% - 0.01px - 1em);flex-basis:calc(25% - 0.01px - var(--grid-gap, 1em));max-width:calc(25% - 0.01px - 1em);max-width:calc(25% - 0.01px - var(--grid-gap, 1em))}.col-4\@sm{-ms-flex-preferred-size:calc(33.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(33.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(33.33% - 0.01px - 1em);flex-basis:calc(33.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(33.33% - 0.01px - 1em);max-width:calc(33.33% - 0.01px - var(--grid-gap, 1em))}.col-5\@sm{-ms-flex-preferred-size:calc(41.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(41.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(41.66% - 0.01px - 1em);flex-basis:calc(41.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(41.66% - 0.01px - 1em);max-width:calc(41.66% - 0.01px - var(--grid-gap, 1em))}.col-6\@sm{-ms-flex-preferred-size:calc(50% - 0.01px - 1em);-ms-flex-preferred-size:calc(50% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(50% - 0.01px - 1em);flex-basis:calc(50% - 0.01px - var(--grid-gap, 1em));max-width:calc(50% - 0.01px - 1em);max-width:calc(50% - 0.01px - var(--grid-gap, 1em))}.col-7\@sm{-ms-flex-preferred-size:calc(58.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(58.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(58.33% - 0.01px - 1em);flex-basis:calc(58.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(58.33% - 0.01px - 1em);max-width:calc(58.33% - 0.01px - var(--grid-gap, 1em))}.col-8\@sm{-ms-flex-preferred-size:calc(66.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(66.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(66.66% - 0.01px - 1em);flex-basis:calc(66.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(66.66% - 0.01px - 1em);max-width:calc(66.66% - 0.01px - var(--grid-gap, 1em))}.col-9\@sm{-ms-flex-preferred-size:calc(75% - 0.01px - 1em);-ms-flex-preferred-size:calc(75% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(75% - 0.01px - 1em);flex-basis:calc(75% - 0.01px - var(--grid-gap, 1em));max-width:calc(75% - 0.01px - 1em);max-width:calc(75% - 0.01px - var(--grid-gap, 1em))}.col-10\@sm{-ms-flex-preferred-size:calc(83.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(83.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(83.33% - 0.01px - 1em);flex-basis:calc(83.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(83.33% - 0.01px - 1em);max-width:calc(83.33% - 0.01px - var(--grid-gap, 1em))}.col-11\@sm{-ms-flex-preferred-size:calc(91.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(91.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(91.66% - 0.01px - 1em);flex-basis:calc(91.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(91.66% - 0.01px - 1em);max-width:calc(91.66% - 0.01px - var(--grid-gap, 1em))}.col-12\@sm{-ms-flex-preferred-size:calc(100% - 0.01px - 1em);-ms-flex-preferred-size:calc(100% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(100% - 0.01px - 1em);flex-basis:calc(100% - 0.01px - var(--grid-gap, 1em));max-width:calc(100% - 0.01px - 1em);max-width:calc(100% - 0.01px - var(--grid-gap, 1em))}}@media (min-width: 64rem){.col\@md{-ms-flex-positive:1;flex-grow:1;-ms-flex-preferred-size:0;flex-basis:0;max-width:100%}.col-1\@md{-ms-flex-preferred-size:calc(8.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(8.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(8.33% - 0.01px - 1em);flex-basis:calc(8.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(8.33% - 0.01px - 1em);max-width:calc(8.33% - 0.01px - var(--grid-gap, 1em))}.col-2\@md{-ms-flex-preferred-size:calc(16.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(16.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(16.66% - 0.01px - 1em);flex-basis:calc(16.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(16.66% - 0.01px - 1em);max-width:calc(16.66% - 0.01px - var(--grid-gap, 1em))}.col-3\@md{-ms-flex-preferred-size:calc(25% - 0.01px - 1em);-ms-flex-preferred-size:calc(25% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(25% - 0.01px - 1em);flex-basis:calc(25% - 0.01px - var(--grid-gap, 1em));max-width:calc(25% - 0.01px - 1em);max-width:calc(25% - 0.01px - var(--grid-gap, 1em))}.col-4\@md{-ms-flex-preferred-size:calc(33.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(33.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(33.33% - 0.01px - 1em);flex-basis:calc(33.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(33.33% - 0.01px - 1em);max-width:calc(33.33% - 0.01px - var(--grid-gap, 1em))}.col-5\@md{-ms-flex-preferred-size:calc(41.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(41.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(41.66% - 0.01px - 1em);flex-basis:calc(41.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(41.66% - 0.01px - 1em);max-width:calc(41.66% - 0.01px - var(--grid-gap, 1em))}.col-6\@md{-ms-flex-preferred-size:calc(50% - 0.01px - 1em);-ms-flex-preferred-size:calc(50% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(50% - 0.01px - 1em);flex-basis:calc(50% - 0.01px - var(--grid-gap, 1em));max-width:calc(50% - 0.01px - 1em);max-width:calc(50% - 0.01px - var(--grid-gap, 1em))}.col-7\@md{-ms-flex-preferred-size:calc(58.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(58.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(58.33% - 0.01px - 1em);flex-basis:calc(58.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(58.33% - 0.01px - 1em);max-width:calc(58.33% - 0.01px - var(--grid-gap, 1em))}.col-8\@md{-ms-flex-preferred-size:calc(66.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(66.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(66.66% - 0.01px - 1em);flex-basis:calc(66.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(66.66% - 0.01px - 1em);max-width:calc(66.66% - 0.01px - var(--grid-gap, 1em))}.col-9\@md{-ms-flex-preferred-size:calc(75% - 0.01px - 1em);-ms-flex-preferred-size:calc(75% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(75% - 0.01px - 1em);flex-basis:calc(75% - 0.01px - var(--grid-gap, 1em));max-width:calc(75% - 0.01px - 1em);max-width:calc(75% - 0.01px - var(--grid-gap, 1em))}.col-10\@md{-ms-flex-preferred-size:calc(83.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(83.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(83.33% - 0.01px - 1em);flex-basis:calc(83.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(83.33% - 0.01px - 1em);max-width:calc(83.33% - 0.01px - var(--grid-gap, 1em))}.col-11\@md{-ms-flex-preferred-size:calc(91.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(91.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(91.66% - 0.01px - 1em);flex-basis:calc(91.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(91.66% - 0.01px - 1em);max-width:calc(91.66% - 0.01px - var(--grid-gap, 1em))}.col-12\@md{-ms-flex-preferred-size:calc(100% - 0.01px - 1em);-ms-flex-preferred-size:calc(100% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(100% - 0.01px - 1em);flex-basis:calc(100% - 0.01px - var(--grid-gap, 1em));max-width:calc(100% - 0.01px - 1em);max-width:calc(100% - 0.01px - var(--grid-gap, 1em))}}@media (min-width: 80rem){.col\@lg{-ms-flex-positive:1;flex-grow:1;-ms-flex-preferred-size:0;flex-basis:0;max-width:100%}.col-1\@lg{-ms-flex-preferred-size:calc(8.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(8.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(8.33% - 0.01px - 1em);flex-basis:calc(8.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(8.33% - 0.01px - 1em);max-width:calc(8.33% - 0.01px - var(--grid-gap, 1em))}.col-2\@lg{-ms-flex-preferred-size:calc(16.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(16.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(16.66% - 0.01px - 1em);flex-basis:calc(16.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(16.66% - 0.01px - 1em);max-width:calc(16.66% - 0.01px - var(--grid-gap, 1em))}.col-3\@lg{-ms-flex-preferred-size:calc(25% - 0.01px - 1em);-ms-flex-preferred-size:calc(25% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(25% - 0.01px - 1em);flex-basis:calc(25% - 0.01px - var(--grid-gap, 1em));max-width:calc(25% - 0.01px - 1em);max-width:calc(25% - 0.01px - var(--grid-gap, 1em))}.col-4\@lg{-ms-flex-preferred-size:calc(33.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(33.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(33.33% - 0.01px - 1em);flex-basis:calc(33.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(33.33% - 0.01px - 1em);max-width:calc(33.33% - 0.01px - var(--grid-gap, 1em))}.col-5\@lg{-ms-flex-preferred-size:calc(41.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(41.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(41.66% - 0.01px - 1em);flex-basis:calc(41.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(41.66% - 0.01px - 1em);max-width:calc(41.66% - 0.01px - var(--grid-gap, 1em))}.col-6\@lg{-ms-flex-preferred-size:calc(50% - 0.01px - 1em);-ms-flex-preferred-size:calc(50% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(50% - 0.01px - 1em);flex-basis:calc(50% - 0.01px - var(--grid-gap, 1em));max-width:calc(50% - 0.01px - 1em);max-width:calc(50% - 0.01px - var(--grid-gap, 1em))}.col-7\@lg{-ms-flex-preferred-size:calc(58.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(58.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(58.33% - 0.01px - 1em);flex-basis:calc(58.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(58.33% - 0.01px - 1em);max-width:calc(58.33% - 0.01px - var(--grid-gap, 1em))}.col-8\@lg{-ms-flex-preferred-size:calc(66.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(66.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(66.66% - 0.01px - 1em);flex-basis:calc(66.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(66.66% - 0.01px - 1em);max-width:calc(66.66% - 0.01px - var(--grid-gap, 1em))}.col-9\@lg{-ms-flex-preferred-size:calc(75% - 0.01px - 1em);-ms-flex-preferred-size:calc(75% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(75% - 0.01px - 1em);flex-basis:calc(75% - 0.01px - var(--grid-gap, 1em));max-width:calc(75% - 0.01px - 1em);max-width:calc(75% - 0.01px - var(--grid-gap, 1em))}.col-10\@lg{-ms-flex-preferred-size:calc(83.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(83.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(83.33% - 0.01px - 1em);flex-basis:calc(83.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(83.33% - 0.01px - 1em);max-width:calc(83.33% - 0.01px - var(--grid-gap, 1em))}.col-11\@lg{-ms-flex-preferred-size:calc(91.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(91.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(91.66% - 0.01px - 1em);flex-basis:calc(91.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(91.66% - 0.01px - 1em);max-width:calc(91.66% - 0.01px - var(--grid-gap, 1em))}.col-12\@lg{-ms-flex-preferred-size:calc(100% - 0.01px - 1em);-ms-flex-preferred-size:calc(100% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(100% - 0.01px - 1em);flex-basis:calc(100% - 0.01px - var(--grid-gap, 1em));max-width:calc(100% - 0.01px - 1em);max-width:calc(100% - 0.01px - var(--grid-gap, 1em))}}@media (min-width: 90rem){.col\@xl{-ms-flex-positive:1;flex-grow:1;-ms-flex-preferred-size:0;flex-basis:0;max-width:100%}.col-1\@xl{-ms-flex-preferred-size:calc(8.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(8.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(8.33% - 0.01px - 1em);flex-basis:calc(8.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(8.33% - 0.01px - 1em);max-width:calc(8.33% - 0.01px - var(--grid-gap, 1em))}.col-2\@xl{-ms-flex-preferred-size:calc(16.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(16.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(16.66% - 0.01px - 1em);flex-basis:calc(16.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(16.66% - 0.01px - 1em);max-width:calc(16.66% - 0.01px - var(--grid-gap, 1em))}.col-3\@xl{-ms-flex-preferred-size:calc(25% - 0.01px - 1em);-ms-flex-preferred-size:calc(25% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(25% - 0.01px - 1em);flex-basis:calc(25% - 0.01px - var(--grid-gap, 1em));max-width:calc(25% - 0.01px - 1em);max-width:calc(25% - 0.01px - var(--grid-gap, 1em))}.col-4\@xl{-ms-flex-preferred-size:calc(33.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(33.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(33.33% - 0.01px - 1em);flex-basis:calc(33.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(33.33% - 0.01px - 1em);max-width:calc(33.33% - 0.01px - var(--grid-gap, 1em))}.col-5\@xl{-ms-flex-preferred-size:calc(41.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(41.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(41.66% - 0.01px - 1em);flex-basis:calc(41.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(41.66% - 0.01px - 1em);max-width:calc(41.66% - 0.01px - var(--grid-gap, 1em))}.col-6\@xl{-ms-flex-preferred-size:calc(50% - 0.01px - 1em);-ms-flex-preferred-size:calc(50% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(50% - 0.01px - 1em);flex-basis:calc(50% - 0.01px - var(--grid-gap, 1em));max-width:calc(50% - 0.01px - 1em);max-width:calc(50% - 0.01px - var(--grid-gap, 1em))}.col-7\@xl{-ms-flex-preferred-size:calc(58.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(58.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(58.33% - 0.01px - 1em);flex-basis:calc(58.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(58.33% - 0.01px - 1em);max-width:calc(58.33% - 0.01px - var(--grid-gap, 1em))}.col-8\@xl{-ms-flex-preferred-size:calc(66.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(66.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(66.66% - 0.01px - 1em);flex-basis:calc(66.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(66.66% - 0.01px - 1em);max-width:calc(66.66% - 0.01px - var(--grid-gap, 1em))}.col-9\@xl{-ms-flex-preferred-size:calc(75% - 0.01px - 1em);-ms-flex-preferred-size:calc(75% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(75% - 0.01px - 1em);flex-basis:calc(75% - 0.01px - var(--grid-gap, 1em));max-width:calc(75% - 0.01px - 1em);max-width:calc(75% - 0.01px - var(--grid-gap, 1em))}.col-10\@xl{-ms-flex-preferred-size:calc(83.33% - 0.01px - 1em);-ms-flex-preferred-size:calc(83.33% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(83.33% - 0.01px - 1em);flex-basis:calc(83.33% - 0.01px - var(--grid-gap, 1em));max-width:calc(83.33% - 0.01px - 1em);max-width:calc(83.33% - 0.01px - var(--grid-gap, 1em))}.col-11\@xl{-ms-flex-preferred-size:calc(91.66% - 0.01px - 1em);-ms-flex-preferred-size:calc(91.66% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(91.66% - 0.01px - 1em);flex-basis:calc(91.66% - 0.01px - var(--grid-gap, 1em));max-width:calc(91.66% - 0.01px - 1em);max-width:calc(91.66% - 0.01px - var(--grid-gap, 1em))}.col-12\@xl{-ms-flex-preferred-size:calc(100% - 0.01px - 1em);-ms-flex-preferred-size:calc(100% - 0.01px - var(--grid-gap, 1em));flex-basis:calc(100% - 0.01px - 1em);flex-basis:calc(100% - 0.01px - var(--grid-gap, 1em));max-width:calc(100% - 0.01px - 1em);max-width:calc(100% - 0.01px - var(--grid-gap, 1em))}}:root{--radius-sm: calc(var(--radius, 0.25em)/2);--radius-md: var(--radius, 0.25em);--radius-lg: calc(var(--radius, 0.25em)*2);--shadow-sm: 0 1px 2px rgba(0, 0, 0, .085), 0 1px 8px rgba(0, 0, 0, .1);--shadow-md: 0 1px 8px rgba(0, 0, 0, .1), 0 8px 24px rgba(0, 0, 0, .15);--shadow-lg: 0 1px 8px rgba(0, 0, 0, .1), 0 16px 48px rgba(0, 0, 0, .1), 0 24px 60px rgba(0, 0, 0, .1);--bounce: cubic-bezier(0.175, 0.885, 0.32, 1.275);--ease-in-out: cubic-bezier(0.645, 0.045, 0.355, 1);--ease-in: cubic-bezier(0.55, 0.055, 0.675, 0.19);--ease-out: cubic-bezier(0.215, 0.61, 0.355, 1)}:root{--body-line-height: 1.4;--heading-line-height: 1.2}body{font-size:1em;font-size:var(--text-base-size, 1em);font-family:'Droid Serif', serif;font-family:var(--font-primary, sans-serif);color:hsl(240, 4%, 20%);color:var(--color-contrast-high, #313135)}h1,h2,h3,h4{color:hsl(240, 8%, 12%);color:var(--color-contrast-higher, #1c1c21);line-height:1.2;line-height:var(--heading-line-height, 1.2)}.text-xxxl{font-size:2.48832em;font-size:var(--text-xxxl, 2.488em)}h1,.text-xxl{font-size:2.0736em;font-size:var(--text-xxl, 2.074em)}h2,.text-xl{font-size:1.728em;font-size:var(--text-xl, 1.728em)}h3,.text-lg{font-size:1.44em;font-size:var(--text-lg, 1.44em)}h4,.text-md{font-size:1.2em;font-size:var(--text-md, 1.2em)}small,.text-sm{font-size:0.83333em;font-size:var(--text-sm, 0.833em)}.text-xs{font-size:0.69444em;font-size:var(--text-xs, 0.694em)}a,.link{color:hsl(220, 90%, 56%);color:var(--color-primary, #2a6df4);text-decoration:underline}strong,.text-bold{font-weight:bold}s{text-decoration:line-through}u,.text-underline{text-decoration:underline}.text-component{--component-body-line-height: calc(var(--body-line-height)*var(--line-height-multiplier, 1));--component-heading-line-height: calc(var(--heading-line-height)*var(--line-height-multiplier, 1))}.text-component h1,.text-component h2,.text-component h3,.text-component h4{line-height:1.2;line-height:var(--component-heading-line-height, 1.2);margin-bottom:0.25em;margin-bottom:calc(var(--space-xxxs)*var(--text-vspace-multiplier, 1))}.text-component h2,.text-component h3,.text-component h4{margin-top:0.75em;margin-top:calc(var(--space-sm)*var(--text-vspace-multiplier, 1))}.text-component p,.text-component blockquote,.text-component ul li,.text-component ol li{line-height:1.4;line-height:var(--component-body-line-height)}.text-component ul,.text-component ol,.text-component p,.text-component blockquote,.text-component .text-component__block{margin-bottom:0.75em;margin-bottom:calc(var(--space-sm)*var(--text-vspace-multiplier, 1))}.text-component ul,.text-component ol{padding-left:1em}.text-component ul{list-style-type:disc}.text-component ol{list-style-type:decimal}.text-component img{display:block;margin:0 auto}.text-component figcaption{text-align:center;margin-top:0.5em;margin-top:var(--space-xs)}.text-component em{font-style:italic}.text-component hr{margin-top:2em;margin-top:calc(var(--space-lg)*var(--text-vspace-multiplier, 1));margin-bottom:2em;margin-bottom:calc(var(--space-lg)*var(--text-vspace-multiplier, 1));margin-left:auto;margin-right:auto}.text-component>*:first-child{margin-top:0}.text-component>*:last-child{margin-bottom:0}.text-component__block--full-width{width:100vw;margin-left:calc(50% - 50vw)}@media (min-width: 48rem){.text-component__block--left,.text-component__block--right{width:45%}.text-component__block--left img,.text-component__block--right img{width:100%}.text-component__block--left{float:left;margin-right:0.75em;margin-right:calc(var(--space-sm)*var(--text-vspace-multiplier, 1))}.text-component__block--right{float:right;margin-left:0.75em;margin-left:calc(var(--space-sm)*var(--text-vspace-multiplier, 1))}}@media (min-width: 90rem){.text-component__block--outset{width:calc(100% + 10.5em);width:calc(100% + 2*var(--space-xxl))}.text-component__block--outset img{width:100%}.text-component__block--outset:not(.text-component__block--right){margin-left:-5.25em;margin-left:calc(-1*var(--space-xxl))}.text-component__block--left,.text-component__block--right{width:50%}.text-component__block--right.text-component__block--outset{margin-right:-5.25em;margin-right:calc(-1*var(--space-xxl))}}:root{--icon-xxs: 12px;--icon-xs: 16px;--icon-sm: 24px;--icon-md: 32px;--icon-lg: 48px;--icon-xl: 64px;--icon-xxl: 128px}.icon{display:inline-block;color:inherit;fill:currentColor;height:1em;width:1em;line-height:1;-ms-flex-negative:0;flex-shrink:0}.icon--xxs{font-size:12px;font-size:var(--icon-xxs)}.icon--xs{font-size:16px;font-size:var(--icon-xs)}.icon--sm{font-size:24px;font-size:var(--icon-sm)}.icon--md{font-size:32px;font-size:var(--icon-md)}.icon--lg{font-size:48px;font-size:var(--icon-lg)}.icon--xl{font-size:64px;font-size:var(--icon-xl)}.icon--xxl{font-size:128px;font-size:var(--icon-xxl)}.icon--is-spinning{-webkit-animation:icon-spin 1s infinite linear;animation:icon-spin 1s infinite linear}@-webkit-keyframes icon-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes icon-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.icon use{color:inherit;fill:currentColor}.btn{position:relative;display:-ms-inline-flexbox;display:inline-flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center;white-space:nowrap;text-decoration:none;line-height:1;font-size:1em;font-size:var(--btn-font-size, 1em);padding-top:0.5em;padding-top:var(--btn-padding-y, 0.5em);padding-bottom:0.5em;padding-bottom:var(--btn-padding-y, 0.5em);padding-left:0.75em;padding-left:var(--btn-padding-x, 0.75em);padding-right:0.75em;padding-right:var(--btn-padding-x, 0.75em);border-radius:0.25em;border-radius:var(--btn-radius, 0.25em)}.btn--primary{background-color:hsl(220, 90%, 56%);background-color:var(--color-primary, #2a6df4);color:hsl(0, 0%, 100%);color:var(--color-white, #fff)}.btn--subtle{background-color:hsl(240, 1%, 83%);background-color:var(--color-contrast-low, #d3d3d4);color:hsl(240, 8%, 12%);color:var(--color-contrast-higher, #1c1c21)}.btn--accent{background-color:hsl(355, 90%, 61%);background-color:var(--color-accent, #f54251);color:hsl(0, 0%, 100%);color:var(--color-white, #fff)}.btn--disabled{cursor:not-allowed}.btn--sm{font-size:0.8em;font-size:var(--btn-font-size-sm, 0.8em)}.btn--md{font-size:1.2em;font-size:var(--btn-font-size-md, 1.2em)}.btn--lg{font-size:1.4em;font-size:var(--btn-font-size-lg, 1.4em)}.btn--icon{padding:0.5em;padding:var(--btn-padding-y, 0.5em)}.form-control{background-color:hsl(0, 0%, 100%);background-color:var(--color-bg, #f2f2f2);padding-top:0.5em;padding-top:var(--form-control-padding-y, 0.5em);padding-bottom:0.5em;padding-bottom:var(--form-control-padding-y, 0.5em);padding-left:0.75em;padding-left:var(--form-control-padding-x, 0.75em);padding-right:0.75em;padding-right:var(--form-control-padding-x, 0.75em);border-radius:0.25em;border-radius:var(--form-control-radius, 0.25em)}.form-control::-webkit-input-placeholder{color:hsl(240, 1%, 48%);color:var(--color-contrast-medium, #79797c)}.form-control::-moz-placeholder{opacity:1;color:hsl(240, 1%, 48%);color:var(--color-contrast-medium, #79797c)}.form-control:-ms-input-placeholder{color:hsl(240, 1%, 48%);color:var(--color-contrast-medium, #79797c)}.form-control:-moz-placeholder{color:hsl(240, 1%, 48%);color:var(--color-contrast-medium, #79797c)}.form-control[disabled],.form-control[readonly]{cursor:not-allowed}.form-legend{color:hsl(240, 8%, 12%);color:var(--color-contrast-higher, #1c1c21);line-height:1.2;font-size:1.2em;font-size:var(--text-md, 1.2em);margin-bottom:0.375em;margin-bottom:var(--space-xxs)}.form-label{display:inline-block}.form__msg-error{background-color:hsl(355, 90%, 61%);background-color:var(--color-error, #f54251);color:hsl(0, 0%, 100%);color:var(--color-white, #fff);font-size:0.83333em;font-size:var(--text-sm, 0.833em);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;padding:0.5em;padding:var(--space-xs);margin-top:0.75em;margin-top:var(--space-sm);border-radius:0.25em;border-radius:var(--radius-md, 0.25em);position:absolute;clip:rect(1px, 1px, 1px, 1px)}.form__msg-error::before{content:'';position:absolute;left:0.75em;left:var(--space-sm);top:0;-webkit-transform:translateY(-100%);-ms-transform:translateY(-100%);transform:translateY(-100%);width:0;height:0;border:8px solid transparent;border-bottom-color:hsl(355, 90%, 61%);border-bottom-color:var(--color-error)}.form__msg-error--is-visible{position:relative;clip:auto}.radio-list>*,.checkbox-list>*{position:relative;display:-ms-flexbox;display:flex;-ms-flex-align:baseline;align-items:baseline;margin-bottom:0.375em;margin-bottom:var(--space-xxs)}.radio-list>*:last-of-type,.checkbox-list>*:last-of-type{margin-bottom:0}.radio-list label,.checkbox-list label{line-height:1.4;line-height:var(--body-line-height);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.radio-list input,.checkbox-list input{vertical-align:top;margin-right:0.25em;margin-right:var(--space-xxxs);-ms-flex-negative:0;flex-shrink:0}:root{--zindex-header: 2;--zindex-popover: 5;--zindex-fixed-element: 10;--zindex-overlay: 15}@media not all and (min-width: 32rem){.display\@xs{display:none !important}}@media (min-width: 32rem){.hide\@xs{display:none !important}}@media not all and (min-width: 48rem){.display\@sm{display:none !important}}@media (min-width: 48rem){.hide\@sm{display:none !important}}@media not all and (min-width: 64rem){.display\@md{display:none !important}}@media (min-width: 64rem){.hide\@md{display:none !important}}@media not all and (min-width: 80rem){.display\@lg{display:none !important}}@media (min-width: 80rem){.hide\@lg{display:none !important}}@media not all and (min-width: 90rem){.display\@xl{display:none !important}}@media (min-width: 90rem){.hide\@xl{display:none !important}}:root{--display: block}.is-visible{display:block !important;display:var(--display) !important}.is-hidden{display:none !important}.sr-only{position:absolute;clip:rect(1px, 1px, 1px, 1px);-webkit-clip-path:inset(50%);clip-path:inset(50%);width:1px;height:1px;overflow:hidden;padding:0;border:0;white-space:nowrap}.flex{display:-ms-flexbox;display:flex}.inline-flex{display:-ms-inline-flexbox;display:inline-flex}.flex-wrap{-ms-flex-wrap:wrap;flex-wrap:wrap}.flex-column{-ms-flex-direction:column;flex-direction:column}.flex-row{-ms-flex-direction:row;flex-direction:row}.flex-center{-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center}.justify-start{-ms-flex-pack:start;justify-content:flex-start}.justify-end{-ms-flex-pack:end;justify-content:flex-end}.justify-center{-ms-flex-pack:center;justify-content:center}.justify-between{-ms-flex-pack:justify;justify-content:space-between}.items-center{-ms-flex-align:center;align-items:center}.items-start{-ms-flex-align:start;align-items:flex-start}.items-end{-ms-flex-align:end;align-items:flex-end}@media (min-width: 32rem){.flex-wrap\@xs{-ms-flex-wrap:wrap;flex-wrap:wrap}.flex-column\@xs{-ms-flex-direction:column;flex-direction:column}.flex-row\@xs{-ms-flex-direction:row;flex-direction:row}.flex-center\@xs{-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center}.justify-start\@xs{-ms-flex-pack:start;justify-content:flex-start}.justify-end\@xs{-ms-flex-pack:end;justify-content:flex-end}.justify-center\@xs{-ms-flex-pack:center;justify-content:center}.justify-between\@xs{-ms-flex-pack:justify;justify-content:space-between}.items-center\@xs{-ms-flex-align:center;align-items:center}.items-start\@xs{-ms-flex-align:start;align-items:flex-start}.items-end\@xs{-ms-flex-align:end;align-items:flex-end}}@media (min-width: 48rem){.flex-wrap\@sm{-ms-flex-wrap:wrap;flex-wrap:wrap}.flex-column\@sm{-ms-flex-direction:column;flex-direction:column}.flex-row\@sm{-ms-flex-direction:row;flex-direction:row}.flex-center\@sm{-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center}.justify-start\@sm{-ms-flex-pack:start;justify-content:flex-start}.justify-end\@sm{-ms-flex-pack:end;justify-content:flex-end}.justify-center\@sm{-ms-flex-pack:center;justify-content:center}.justify-between\@sm{-ms-flex-pack:justify;justify-content:space-between}.items-center\@sm{-ms-flex-align:center;align-items:center}.items-start\@sm{-ms-flex-align:start;align-items:flex-start}.items-end\@sm{-ms-flex-align:end;align-items:flex-end}}@media (min-width: 64rem){.flex-wrap\@md{-ms-flex-wrap:wrap;flex-wrap:wrap}.flex-column\@md{-ms-flex-direction:column;flex-direction:column}.flex-row\@md{-ms-flex-direction:row;flex-direction:row}.flex-center\@md{-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center}.justify-start\@md{-ms-flex-pack:start;justify-content:flex-start}.justify-end\@md{-ms-flex-pack:end;justify-content:flex-end}.justify-center\@md{-ms-flex-pack:center;justify-content:center}.justify-between\@md{-ms-flex-pack:justify;justify-content:space-between}.items-center\@md{-ms-flex-align:center;align-items:center}.items-start\@md{-ms-flex-align:start;align-items:flex-start}.items-end\@md{-ms-flex-align:end;align-items:flex-end}}@media (min-width: 80rem){.flex-wrap\@lg{-ms-flex-wrap:wrap;flex-wrap:wrap}.flex-column\@lg{-ms-flex-direction:column;flex-direction:column}.flex-row\@lg{-ms-flex-direction:row;flex-direction:row}.flex-center\@lg{-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center}.justify-start\@lg{-ms-flex-pack:start;justify-content:flex-start}.justify-end\@lg{-ms-flex-pack:end;justify-content:flex-end}.justify-center\@lg{-ms-flex-pack:center;justify-content:center}.justify-between\@lg{-ms-flex-pack:justify;justify-content:space-between}.items-center\@lg{-ms-flex-align:center;align-items:center}.items-start\@lg{-ms-flex-align:start;align-items:flex-start}.items-end\@lg{-ms-flex-align:end;align-items:flex-end}}@media (min-width: 90rem){.flex-wrap\@xl{-ms-flex-wrap:wrap;flex-wrap:wrap}.flex-column\@xl{-ms-flex-direction:column;flex-direction:column}.flex-row\@xl{-ms-flex-direction:row;flex-direction:row}.flex-center\@xl{-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center}.justify-start\@xl{-ms-flex-pack:start;justify-content:flex-start}.justify-end\@xl{-ms-flex-pack:end;justify-content:flex-end}.justify-center\@xl{-ms-flex-pack:center;justify-content:center}.justify-between\@xl{-ms-flex-pack:justify;justify-content:space-between}.items-center\@xl{-ms-flex-align:center;align-items:center}.items-start\@xl{-ms-flex-align:start;align-items:flex-start}.items-end\@xl{-ms-flex-align:end;align-items:flex-end}}.flex-grow{-ms-flex-positive:1;flex-grow:1}.flex-shrink-0{-ms-flex-negative:0;flex-shrink:0}.flex-gap-xxxs{margin-bottom:-0.25em;margin-bottom:calc(-1*var(--space-xxxs));margin-right:-0.25em;margin-right:calc(-1*var(--space-xxxs))}.flex-gap-xxxs>*{margin-bottom:0.25em;margin-bottom:var(--space-xxxs);margin-right:0.25em;margin-right:var(--space-xxxs)}.flex-gap-xxs{margin-bottom:-0.375em;margin-bottom:calc(-1*var(--space-xxs));margin-right:-0.375em;margin-right:calc(-1*var(--space-xxs))}.flex-gap-xxs>*{margin-bottom:0.375em;margin-bottom:var(--space-xxs);margin-right:0.375em;margin-right:var(--space-xxs)}.flex-gap-xs{margin-bottom:-0.5em;margin-bottom:calc(-1*var(--space-xs));margin-right:-0.5em;margin-right:calc(-1*var(--space-xs))}.flex-gap-xs>*{margin-bottom:0.5em;margin-bottom:var(--space-xs);margin-right:0.5em;margin-right:var(--space-xs)}.flex-gap-sm{margin-bottom:-0.75em;margin-bottom:calc(-1*var(--space-sm));margin-right:-0.75em;margin-right:calc(-1*var(--space-sm))}.flex-gap-sm>*{margin-bottom:0.75em;margin-bottom:var(--space-sm);margin-right:0.75em;margin-right:var(--space-sm)}.flex-gap-md{margin-bottom:-1.25em;margin-bottom:calc(-1*var(--space-md));margin-right:-1.25em;margin-right:calc(-1*var(--space-md))}.flex-gap-md>*{margin-bottom:1.25em;margin-bottom:var(--space-md);margin-right:1.25em;margin-right:var(--space-md)}.flex-gap-lg{margin-bottom:-2em;margin-bottom:calc(-1*var(--space-lg));margin-right:-2em;margin-right:calc(-1*var(--space-lg))}.flex-gap-lg>*{margin-bottom:2em;margin-bottom:var(--space-lg);margin-right:2em;margin-right:var(--space-lg)}.flex-gap-xl{margin-bottom:-3.25em;margin-bottom:calc(-1*var(--space-xl));margin-right:-3.25em;margin-right:calc(-1*var(--space-xl))}.flex-gap-xl>*{margin-bottom:3.25em;margin-bottom:var(--space-xl);margin-right:3.25em;margin-right:var(--space-xl)}.flex-gap-xxl{margin-bottom:-5.25em;margin-bottom:calc(-1*var(--space-xxl));margin-right:-5.25em;margin-right:calc(-1*var(--space-xxl))}.flex-gap-xxl>*{margin-bottom:5.25em;margin-bottom:var(--space-xxl);margin-right:5.25em;margin-right:var(--space-xxl)}.margin-xxxxs{margin:0.125em;margin:var(--space-xxxxs)}.margin-xxxs{margin:0.25em;margin:var(--space-xxxs)}.margin-xxs{margin:0.375em;margin:var(--space-xxs)}.margin-xs{margin:0.5em;margin:var(--space-xs)}.margin-sm{margin:0.75em;margin:var(--space-sm)}.margin-md{margin:1.25em;margin:var(--space-md)}.margin-lg{margin:2em;margin:var(--space-lg)}.margin-xl{margin:3.25em;margin:var(--space-xl)}.margin-xxl{margin:5.25em;margin:var(--space-xxl)}.margin-xxxl{margin:8.5em;margin:var(--space-xxxl)}.margin-xxxxl{margin:13.75em;margin:var(--space-xxxxl)}.margin-auto{margin:auto}.margin-top-xxxxs{margin-top:0.125em;margin-top:var(--space-xxxxs)}.margin-top-xxxs{margin-top:0.25em;margin-top:var(--space-xxxs)}.margin-top-xxs{margin-top:0.375em;margin-top:var(--space-xxs)}.margin-top-xs{margin-top:0.5em;margin-top:var(--space-xs)}.margin-top-sm{margin-top:0.75em;margin-top:var(--space-sm)}.margin-top-md{margin-top:1.25em;margin-top:var(--space-md)}.margin-top-lg{margin-top:2em;margin-top:var(--space-lg)}.margin-top-xl{margin-top:3.25em;margin-top:var(--space-xl)}.margin-top-xxl{margin-top:5.25em;margin-top:var(--space-xxl)}.margin-top-xxxl{margin-top:8.5em;margin-top:var(--space-xxxl)}.margin-top-xxxxl{margin-top:13.75em;margin-top:var(--space-xxxxl)}.margin-top-auto{margin-top:auto}.margin-bottom-xxxxs{margin-bottom:0.125em;margin-bottom:var(--space-xxxxs)}.margin-bottom-xxxs{margin-bottom:0.25em;margin-bottom:var(--space-xxxs)}.margin-bottom-xxs{margin-bottom:0.375em;margin-bottom:var(--space-xxs)}.margin-bottom-xs{margin-bottom:0.5em;margin-bottom:var(--space-xs)}.margin-bottom-sm{margin-bottom:0.75em;margin-bottom:var(--space-sm)}.margin-bottom-md{margin-bottom:1.25em;margin-bottom:var(--space-md)}.margin-bottom-lg{margin-bottom:2em;margin-bottom:var(--space-lg)}.margin-bottom-xl{margin-bottom:3.25em;margin-bottom:var(--space-xl)}.margin-bottom-xxl{margin-bottom:5.25em;margin-bottom:var(--space-xxl)}.margin-bottom-xxxl{margin-bottom:8.5em;margin-bottom:var(--space-xxxl)}.margin-bottom-xxxxl{margin-bottom:13.75em;margin-bottom:var(--space-xxxxl)}.margin-bottom-auto{margin-bottom:auto}.margin-right-xxxxs{margin-right:0.125em;margin-right:var(--space-xxxxs)}.margin-right-xxxs{margin-right:0.25em;margin-right:var(--space-xxxs)}.margin-right-xxs{margin-right:0.375em;margin-right:var(--space-xxs)}.margin-right-xs{margin-right:0.5em;margin-right:var(--space-xs)}.margin-right-sm{margin-right:0.75em;margin-right:var(--space-sm)}.margin-right-md{margin-right:1.25em;margin-right:var(--space-md)}.margin-right-lg{margin-right:2em;margin-right:var(--space-lg)}.margin-right-xl{margin-right:3.25em;margin-right:var(--space-xl)}.margin-right-xxl{margin-right:5.25em;margin-right:var(--space-xxl)}.margin-right-xxxl{margin-right:8.5em;margin-right:var(--space-xxxl)}.margin-right-xxxxl{margin-right:13.75em;margin-right:var(--space-xxxxl)}.margin-right-auto{margin-right:auto}.margin-left-xxxxs{margin-left:0.125em;margin-left:var(--space-xxxxs)}.margin-left-xxxs{margin-left:0.25em;margin-left:var(--space-xxxs)}.margin-left-xxs{margin-left:0.375em;margin-left:var(--space-xxs)}.margin-left-xs{margin-left:0.5em;margin-left:var(--space-xs)}.margin-left-sm{margin-left:0.75em;margin-left:var(--space-sm)}.margin-left-md{margin-left:1.25em;margin-left:var(--space-md)}.margin-left-lg{margin-left:2em;margin-left:var(--space-lg)}.margin-left-xl{margin-left:3.25em;margin-left:var(--space-xl)}.margin-left-xxl{margin-left:5.25em;margin-left:var(--space-xxl)}.margin-left-xxxl{margin-left:8.5em;margin-left:var(--space-xxxl)}.margin-left-xxxxl{margin-left:13.75em;margin-left:var(--space-xxxxl)}.margin-left-auto{margin-left:auto}.margin-x-xxxxs{margin-left:0.125em;margin-left:var(--space-xxxxs);margin-right:0.125em;margin-right:var(--space-xxxxs)}.margin-x-xxxs{margin-left:0.25em;margin-left:var(--space-xxxs);margin-right:0.25em;margin-right:var(--space-xxxs)}.margin-x-xxs{margin-left:0.375em;margin-left:var(--space-xxs);margin-right:0.375em;margin-right:var(--space-xxs)}.margin-x-xs{margin-left:0.5em;margin-left:var(--space-xs);margin-right:0.5em;margin-right:var(--space-xs)}.margin-x-sm{margin-left:0.75em;margin-left:var(--space-sm);margin-right:0.75em;margin-right:var(--space-sm)}.margin-x-md{margin-left:1.25em;margin-left:var(--space-md);margin-right:1.25em;margin-right:var(--space-md)}.margin-x-lg{margin-left:2em;margin-left:var(--space-lg);margin-right:2em;margin-right:var(--space-lg)}.margin-x-xl{margin-left:3.25em;margin-left:var(--space-xl);margin-right:3.25em;margin-right:var(--space-xl)}.margin-x-xxl{margin-left:5.25em;margin-left:var(--space-xxl);margin-right:5.25em;margin-right:var(--space-xxl)}.margin-x-xxxl{margin-left:8.5em;margin-left:var(--space-xxxl);margin-right:8.5em;margin-right:var(--space-xxxl)}.margin-x-xxxxl{margin-left:13.75em;margin-left:var(--space-xxxxl);margin-right:13.75em;margin-right:var(--space-xxxxl)}.margin-x-auto{margin-left:auto;margin-right:auto}.margin-y-xxxxs{margin-top:0.125em;margin-top:var(--space-xxxxs);margin-bottom:0.125em;margin-bottom:var(--space-xxxxs)}.margin-y-xxxs{margin-top:0.25em;margin-top:var(--space-xxxs);margin-bottom:0.25em;margin-bottom:var(--space-xxxs)}.margin-y-xxs{margin-top:0.375em;margin-top:var(--space-xxs);margin-bottom:0.375em;margin-bottom:var(--space-xxs)}.margin-y-xs{margin-top:0.5em;margin-top:var(--space-xs);margin-bottom:0.5em;margin-bottom:var(--space-xs)}.margin-y-sm{margin-top:0.75em;margin-top:var(--space-sm);margin-bottom:0.75em;margin-bottom:var(--space-sm)}.margin-y-md{margin-top:1.25em;margin-top:var(--space-md);margin-bottom:1.25em;margin-bottom:var(--space-md)}.margin-y-lg{margin-top:2em;margin-top:var(--space-lg);margin-bottom:2em;margin-bottom:var(--space-lg)}.margin-y-xl{margin-top:3.25em;margin-top:var(--space-xl);margin-bottom:3.25em;margin-bottom:var(--space-xl)}.margin-y-xxl{margin-top:5.25em;margin-top:var(--space-xxl);margin-bottom:5.25em;margin-bottom:var(--space-xxl)}.margin-y-xxxl{margin-top:8.5em;margin-top:var(--space-xxxl);margin-bottom:8.5em;margin-bottom:var(--space-xxxl)}.margin-y-xxxxl{margin-top:13.75em;margin-top:var(--space-xxxxl);margin-bottom:13.75em;margin-bottom:var(--space-xxxxl)}.margin-y-auto{margin-top:auto;margin-bottom:auto}@media not all and (min-width: 32rem){.has-margin\@xs{margin:0 !important}}@media not all and (min-width: 48rem){.has-margin\@sm{margin:0 !important}}@media not all and (min-width: 64rem){.has-margin\@md{margin:0 !important}}@media not all and (min-width: 80rem){.has-margin\@lg{margin:0 !important}}@media not all and (min-width: 90rem){.has-margin\@xl{margin:0 !important}}.padding-md{padding:1.25em;padding:var(--space-md)}.padding-xxxxs{padding:0.125em;padding:var(--space-xxxxs)}.padding-xxxs{padding:0.25em;padding:var(--space-xxxs)}.padding-xxs{padding:0.375em;padding:var(--space-xxs)}.padding-xs{padding:0.5em;padding:var(--space-xs)}.padding-sm{padding:0.75em;padding:var(--space-sm)}.padding-lg{padding:2em;padding:var(--space-lg)}.padding-xl{padding:3.25em;padding:var(--space-xl)}.padding-xxl{padding:5.25em;padding:var(--space-xxl)}.padding-xxxl{padding:8.5em;padding:var(--space-xxxl)}.padding-xxxxl{padding:13.75em;padding:var(--space-xxxxl)}.padding-component{padding:1.25em;padding:var(--component-padding)}.padding-top-md{padding-top:1.25em;padding-top:var(--space-md)}.padding-top-xxxxs{padding-top:0.125em;padding-top:var(--space-xxxxs)}.padding-top-xxxs{padding-top:0.25em;padding-top:var(--space-xxxs)}.padding-top-xxs{padding-top:0.375em;padding-top:var(--space-xxs)}.padding-top-xs{padding-top:0.5em;padding-top:var(--space-xs)}.padding-top-sm{padding-top:0.75em;padding-top:var(--space-sm)}.padding-top-lg{padding-top:2em;padding-top:var(--space-lg)}.padding-top-xl{padding-top:3.25em;padding-top:var(--space-xl)}.padding-top-xxl{padding-top:5.25em;padding-top:var(--space-xxl)}.padding-top-xxxl{padding-top:8.5em;padding-top:var(--space-xxxl)}.padding-top-xxxxl{padding-top:13.75em;padding-top:var(--space-xxxxl)}.padding-top-component{padding-top:1.25em;padding-top:var(--component-padding)}.padding-bottom-md{padding-bottom:1.25em;padding-bottom:var(--space-md)}.padding-bottom-xxxxs{padding-bottom:0.125em;padding-bottom:var(--space-xxxxs)}.padding-bottom-xxxs{padding-bottom:0.25em;padding-bottom:var(--space-xxxs)}.padding-bottom-xxs{padding-bottom:0.375em;padding-bottom:var(--space-xxs)}.padding-bottom-xs{padding-bottom:0.5em;padding-bottom:var(--space-xs)}.padding-bottom-sm{padding-bottom:0.75em;padding-bottom:var(--space-sm)}.padding-bottom-lg{padding-bottom:2em;padding-bottom:var(--space-lg)}.padding-bottom-xl{padding-bottom:3.25em;padding-bottom:var(--space-xl)}.padding-bottom-xxl{padding-bottom:5.25em;padding-bottom:var(--space-xxl)}.padding-bottom-xxxl{padding-bottom:8.5em;padding-bottom:var(--space-xxxl)}.padding-bottom-xxxxl{padding-bottom:13.75em;padding-bottom:var(--space-xxxxl)}.padding-bottom-component{padding-bottom:1.25em;padding-bottom:var(--component-padding)}.padding-right-md{padding-right:1.25em;padding-right:var(--space-md)}.padding-right-xxxxs{padding-right:0.125em;padding-right:var(--space-xxxxs)}.padding-right-xxxs{padding-right:0.25em;padding-right:var(--space-xxxs)}.padding-right-xxs{padding-right:0.375em;padding-right:var(--space-xxs)}.padding-right-xs{padding-right:0.5em;padding-right:var(--space-xs)}.padding-right-sm{padding-right:0.75em;padding-right:var(--space-sm)}.padding-right-lg{padding-right:2em;padding-right:var(--space-lg)}.padding-right-xl{padding-right:3.25em;padding-right:var(--space-xl)}.padding-right-xxl{padding-right:5.25em;padding-right:var(--space-xxl)}.padding-right-xxxl{padding-right:8.5em;padding-right:var(--space-xxxl)}.padding-right-xxxxl{padding-right:13.75em;padding-right:var(--space-xxxxl)}.padding-right-component{padding-right:1.25em;padding-right:var(--component-padding)}.padding-left-md{padding-left:1.25em;padding-left:var(--space-md)}.padding-left-xxxxs{padding-left:0.125em;padding-left:var(--space-xxxxs)}.padding-left-xxxs{padding-left:0.25em;padding-left:var(--space-xxxs)}.padding-left-xxs{padding-left:0.375em;padding-left:var(--space-xxs)}.padding-left-xs{padding-left:0.5em;padding-left:var(--space-xs)}.padding-left-sm{padding-left:0.75em;padding-left:var(--space-sm)}.padding-left-lg{padding-left:2em;padding-left:var(--space-lg)}.padding-left-xl{padding-left:3.25em;padding-left:var(--space-xl)}.padding-left-xxl{padding-left:5.25em;padding-left:var(--space-xxl)}.padding-left-xxxl{padding-left:8.5em;padding-left:var(--space-xxxl)}.padding-left-xxxxl{padding-left:13.75em;padding-left:var(--space-xxxxl)}.padding-left-component{padding-left:1.25em;padding-left:var(--component-padding)}.padding-x-md{padding-left:1.25em;padding-left:var(--space-md);padding-right:1.25em;padding-right:var(--space-md)}.padding-x-xxxxs{padding-left:0.125em;padding-left:var(--space-xxxxs);padding-right:0.125em;padding-right:var(--space-xxxxs)}.padding-x-xxxs{padding-left:0.25em;padding-left:var(--space-xxxs);padding-right:0.25em;padding-right:var(--space-xxxs)}.padding-x-xxs{padding-left:0.375em;padding-left:var(--space-xxs);padding-right:0.375em;padding-right:var(--space-xxs)}.padding-x-xs{padding-left:0.5em;padding-left:var(--space-xs);padding-right:0.5em;padding-right:var(--space-xs)}.padding-x-sm{padding-left:0.75em;padding-left:var(--space-sm);padding-right:0.75em;padding-right:var(--space-sm)}.padding-x-lg{padding-left:2em;padding-left:var(--space-lg);padding-right:2em;padding-right:var(--space-lg)}.padding-x-xl{padding-left:3.25em;padding-left:var(--space-xl);padding-right:3.25em;padding-right:var(--space-xl)}.padding-x-xxl{padding-left:5.25em;padding-left:var(--space-xxl);padding-right:5.25em;padding-right:var(--space-xxl)}.padding-x-xxxl{padding-left:8.5em;padding-left:var(--space-xxxl);padding-right:8.5em;padding-right:var(--space-xxxl)}.padding-x-xxxxl{padding-left:13.75em;padding-left:var(--space-xxxxl);padding-right:13.75em;padding-right:var(--space-xxxxl)}.padding-x-component{padding-left:1.25em;padding-left:var(--component-padding);padding-right:1.25em;padding-right:var(--component-padding)}.padding-y-md{padding-top:1.25em;padding-top:var(--space-md);padding-bottom:1.25em;padding-bottom:var(--space-md)}.padding-y-xxxxs{padding-top:0.125em;padding-top:var(--space-xxxxs);padding-bottom:0.125em;padding-bottom:var(--space-xxxxs)}.padding-y-xxxs{padding-top:0.25em;padding-top:var(--space-xxxs);padding-bottom:0.25em;padding-bottom:var(--space-xxxs)}.padding-y-xxs{padding-top:0.375em;padding-top:var(--space-xxs);padding-bottom:0.375em;padding-bottom:var(--space-xxs)}.padding-y-xs{padding-top:0.5em;padding-top:var(--space-xs);padding-bottom:0.5em;padding-bottom:var(--space-xs)}.padding-y-sm{padding-top:0.75em;padding-top:var(--space-sm);padding-bottom:0.75em;padding-bottom:var(--space-sm)}.padding-y-lg{padding-top:2em;padding-top:var(--space-lg);padding-bottom:2em;padding-bottom:var(--space-lg)}.padding-y-xl{padding-top:3.25em;padding-top:var(--space-xl);padding-bottom:3.25em;padding-bottom:var(--space-xl)}.padding-y-xxl{padding-top:5.25em;padding-top:var(--space-xxl);padding-bottom:5.25em;padding-bottom:var(--space-xxl)}.padding-y-xxxl{padding-top:8.5em;padding-top:var(--space-xxxl);padding-bottom:8.5em;padding-bottom:var(--space-xxxl)}.padding-y-xxxxl{padding-top:13.75em;padding-top:var(--space-xxxxl);padding-bottom:13.75em;padding-bottom:var(--space-xxxxl)}.padding-y-component{padding-top:1.25em;padding-top:var(--component-padding);padding-bottom:1.25em;padding-bottom:var(--component-padding)}@media not all and (min-width: 32rem){.has-padding\@xs{padding:0 !important}}@media not all and (min-width: 48rem){.has-padding\@sm{padding:0 !important}}@media not all and (min-width: 64rem){.has-padding\@md{padding:0 !important}}@media not all and (min-width: 80rem){.has-padding\@lg{padding:0 !important}}@media not all and (min-width: 90rem){.has-padding\@xl{padding:0 !important}}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-replace{overflow:hidden;color:transparent;text-indent:100%;white-space:nowrap}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}@media (min-width: 32rem){.text-center\@xs{text-align:center}.text-left\@xs{text-align:left}.text-right\@xs{text-align:right}}@media (min-width: 48rem){.text-center\@sm{text-align:center}.text-left\@sm{text-align:left}.text-right\@sm{text-align:right}}@media (min-width: 64rem){.text-center\@md{text-align:center}.text-left\@md{text-align:left}.text-right\@md{text-align:right}}@media (min-width: 80rem){.text-center\@lg{text-align:center}.text-left\@lg{text-align:left}.text-right\@lg{text-align:right}}@media (min-width: 90rem){.text-center\@xl{text-align:center}.text-left\@xl{text-align:left}.text-right\@xl{text-align:right}}.color-inherit{color:inherit}.color-contrast-medium{color:hsl(240, 1%, 48%);color:var(--color-contrast-medium, #79797c)}.color-contrast-high{color:hsl(240, 4%, 20%);color:var(--color-contrast-high, #313135)}.color-contrast-higher{color:hsl(240, 8%, 12%);color:var(--color-contrast-higher, #1c1c21)}.color-primary{color:hsl(220, 90%, 56%);color:var(--color-primary, #2a6df4)}.color-accent{color:hsl(355, 90%, 61%);color:var(--color-accent, #f54251)}.color-success{color:hsl(94, 48%, 56%);color:var(--color-success, #88c559)}.color-warning{color:hsl(46, 100%, 61%);color:var(--color-warning, #ffd138)}.color-error{color:hsl(355, 90%, 61%);color:var(--color-error, #f54251)}.width-100\%{width:100%}.height-100\%{height:100%}.media-wrapper{position:relative;height:0;padding-bottom:56.25%}.media-wrapper iframe,.media-wrapper video,.media-wrapper img{position:absolute;top:0;left:0;width:100%;height:100%}.media-wrapper video,.media-wrapper img{-o-object-fit:cover;object-fit:cover}.media-wrapper--4\:3{padding-bottom:75%}:root,[data-theme="default"]{--color-primary-darker:hsl(220, 90%, 36%);--color-primary-darker-h:220;--color-primary-darker-s:90%;--color-primary-darker-l:36%;--color-primary-dark:hsl(220, 90%, 46%);--color-primary-dark-h:220;--color-primary-dark-s:90%;--color-primary-dark-l:46%;--color-primary:hsl(220, 90%, 56%);--color-primary-h:220;--color-primary-s:90%;--color-primary-l:56%;--color-primary-light:hsl(220, 90%, 66%);--color-primary-light-h:220;--color-primary-light-s:90%;--color-primary-light-l:66%;--color-primary-lighter:hsl(220, 90%, 76%);--color-primary-lighter-h:220;--color-primary-lighter-s:90%;--color-primary-lighter-l:76%;--color-accent-darker:hsl(355, 90%, 41%);--color-accent-darker-h:355;--color-accent-darker-s:90%;--color-accent-darker-l:41%;--color-accent-dark:hsl(355, 90%, 51%);--color-accent-dark-h:355;--color-accent-dark-s:90%;--color-accent-dark-l:51%;--color-accent:hsl(355, 90%, 61%);--color-accent-h:355;--color-accent-s:90%;--color-accent-l:61%;--color-accent-light:hsl(355, 90%, 71%);--color-accent-light-h:355;--color-accent-light-s:90%;--color-accent-light-l:71%;--color-accent-lighter:hsl(355, 90%, 81%);--color-accent-lighter-h:355;--color-accent-lighter-s:90%;--color-accent-lighter-l:81%;--color-black:hsl(240, 8%, 12%);--color-black-h:240;--color-black-s:8%;--color-black-l:12%;--color-white:hsl(0, 0%, 100%);--color-white-h:0;--color-white-s:0%;--color-white-l:100%;--color-success-darker:hsl(94, 48%, 36%);--color-success-darker-h:94;--color-success-darker-s:48%;--color-success-darker-l:36%;--color-success-dark:hsl(94, 48%, 46%);--color-success-dark-h:94;--color-success-dark-s:48%;--color-success-dark-l:46%;--color-success:hsl(94, 48%, 56%);--color-success-h:94;--color-success-s:48%;--color-success-l:56%;--color-success-light:hsl(94, 48%, 66%);--color-success-light-h:94;--color-success-light-s:48%;--color-success-light-l:66%;--color-success-lighter:hsl(94, 48%, 76%);--color-success-lighter-h:94;--color-success-lighter-s:48%;--color-success-lighter-l:76%;--color-error-darker:hsl(355, 90%, 41%);--color-error-darker-h:355;--color-error-darker-s:90%;--color-error-darker-l:41%;--color-error-dark:hsl(355, 90%, 51%);--color-error-dark-h:355;--color-error-dark-s:90%;--color-error-dark-l:51%;--color-error:hsl(355, 90%, 61%);--color-error-h:355;--color-error-s:90%;--color-error-l:61%;--color-error-light:hsl(355, 90%, 71%);--color-error-light-h:355;--color-error-light-s:90%;--color-error-light-l:71%;--color-error-lighter:hsl(355, 90%, 81%);--color-error-lighter-h:355;--color-error-lighter-s:90%;--color-error-lighter-l:81%;--color-warning-darker:hsl(46, 100%, 41%);--color-warning-darker-h:46;--color-warning-darker-s:100%;--color-warning-darker-l:41%;--color-warning-dark:hsl(46, 100%, 51%);--color-warning-dark-h:46;--color-warning-dark-s:100%;--color-warning-dark-l:51%;--color-warning:hsl(46, 100%, 61%);--color-warning-h:46;--color-warning-s:100%;--color-warning-l:61%;--color-warning-light:hsl(46, 100%, 71%);--color-warning-light-h:46;--color-warning-light-s:100%;--color-warning-light-l:71%;--color-warning-lighter:hsl(46, 100%, 81%);--color-warning-lighter-h:46;--color-warning-lighter-s:100%;--color-warning-lighter-l:81%;--color-bg:hsl(0, 0%, 100%);--color-bg-h:0;--color-bg-s:0%;--color-bg-l:100%;--color-contrast-lower:hsl(0, 0%, 95%);--color-contrast-lower-h:0;--color-contrast-lower-s:0%;--color-contrast-lower-l:95%;--color-contrast-low:hsl(240, 1%, 83%);--color-contrast-low-h:240;--color-contrast-low-s:1%;--color-contrast-low-l:83%;--color-contrast-medium:hsl(240, 1%, 48%);--color-contrast-medium-h:240;--color-contrast-medium-s:1%;--color-contrast-medium-l:48%;--color-contrast-high:hsl(240, 4%, 20%);--color-contrast-high-h:240;--color-contrast-high-s:4%;--color-contrast-high-l:20%;--color-contrast-higher:hsl(240, 8%, 12%);--color-contrast-higher-h:240;--color-contrast-higher-s:8%;--color-contrast-higher-l:12%}@supports (--css: variables){@media (min-width: 64rem){:root{--space-unit: 1.25em}}}:root{--radius: 0.25em}:root{--font-primary: sans-serif;--text-base-size: 1em;--text-scale-ratio: 1.2;--text-xs: calc(1em/var(--text-scale-ratio)/var(--text-scale-ratio));--text-sm: calc(var(--text-xs)*var(--text-scale-ratio));--text-md: calc(var(--text-sm)*var(--text-scale-ratio)*var(--text-scale-ratio));--text-lg: calc(var(--text-md)*var(--text-scale-ratio));--text-xl: calc(var(--text-lg)*var(--text-scale-ratio));--text-xxl: calc(var(--text-xl)*var(--text-scale-ratio));--text-xxxl: calc(var(--text-xxl)*var(--text-scale-ratio));--body-line-height: 1.4;--heading-line-height: 1.2;--font-primary-capital-letter: 1}@supports (--css: variables){@media (min-width: 64rem){:root{--text-base-size: 1.25em;--text-scale-ratio: 1.25}}}mark{background-color:hsla(355, 90%, 61%, 0.2);background-color:hsla(var(--color-accent-h), var(--color-accent-s), var(--color-accent-l), 0.2);color:inherit}.text-component{--line-height-multiplier: 1;--text-vspace-multiplier: 1}.text-component blockquote{padding-left:1em;border-left:4px solid hsl(240, 1%, 83%);border-left:4px solid var(--color-contrast-low)}.text-component hr{background:hsl(240, 1%, 83%);background:var(--color-contrast-low);height:1px}.text-component figcaption{font-size:0.83333em;font-size:var(--text-sm);color:hsl(240, 1%, 48%);color:var(--color-contrast-medium)}.article.text-component{--line-height-multiplier: 1.13;--text-vspace-multiplier: 1.2}:root{--btn-font-size: 1em;--btn-font-size-sm: calc(var(--btn-font-size) - 0.2em);--btn-font-size-md: calc(var(--btn-font-size) + 0.2em);--btn-font-size-lg: calc(var(--btn-font-size) + 0.4em);--btn-radius: 0.25em;--btn-padding-x: var(--space-sm);--btn-padding-y: var(--space-xs)}.btn{--color-shadow: hsla(240, 8%, 12%, 0.15);--color-shadow: hsla(var(--color-black-h), var(--color-black-s), var(--color-black-l), 0.15);box-shadow:0 4px 16px hsla(240, 8%, 12%, 0.15);box-shadow:0 4px 16px hsla(var(--color-black-h), var(--color-black-s), var(--color-black-l), 0.15);cursor:pointer}.btn--primary{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.btn--accent{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.btn--disabled{opacity:0.6}:root{--form-control-padding-x: var(--space-sm);--form-control-padding-y: var(--space-xs);--form-control-radius: 0.25em}.form-control{border:2px solid hsl(240, 1%, 83%);border:2px solid var(--color-contrast-low)}.form-control:focus{outline:none;border-color:hsl(220, 90%, 56%);border-color:var(--color-primary);--color-shadow: hsla(220, 90%, 56%, 0.2);--color-shadow: hsla(var(--color-primary-h), var(--color-primary-s), var(--color-primary-l), 0.2);box-shadow:undefined;box-shadow:0 0 0 3px var(--color-shadow)}.form-control:focus:focus{box-shadow:0 0 0 3px hsla(220, 90%, 56%, 0.2);box-shadow:0 0 0 3px var(--color-shadow)}.form-control[aria-invalid="true"]{border-color:hsl(355, 90%, 61%);border-color:var(--color-error)}.form-control[aria-invalid="true"]:focus{--color-shadow: hsla(355, 90%, 61%, 0.2);--color-shadow: hsla(var(--color-error-h), var(--color-error-s), var(--color-error-l), 0.2);box-shadow:undefined;box-shadow:0 0 0 3px var(--color-shadow)}.form-control[aria-invalid="true"]:focus:focus{box-shadow:0 0 0 3px hsla(355, 90%, 61%, 0.2);box-shadow:0 0 0 3px var(--color-shadow)}.form-label{font-size:0.83333em;font-size:var(--text-sm)}:root{--cd-color-1:hsl(206, 21%, 24%);--cd-color-1-h:206;--cd-color-1-s:21%;--cd-color-1-l:24%;--cd-color-2:hsl(205, 38%, 89%);--cd-color-2-h:205;--cd-color-2-s:38%;--cd-color-2-l:89%;--cd-color-3:hsl(207, 10%, 55%);--cd-color-3-h:207;--cd-color-3-s:10%;--cd-color-3-l:55%;--cd-color-4:hsl(111, 51%, 60%);--cd-color-4-h:111;--cd-color-4-s:51%;--cd-color-4-l:60%;--cd-color-5:hsl(356, 53%, 49%);--cd-color-5-h:356;--cd-color-5-s:53%;--cd-color-5-l:49%;--cd-color-6:hsl(47, 85%, 61%);--cd-color-6-h:47;--cd-color-6-s:85%;--cd-color-6-l:61%;--cd-header-height: 200px;--font-primary: 'Droid Serif', serif;--font-secondary: 'Open Sans', sans-serif}@supports (--css: variables){@media (min-width: 64rem){:root{--cd-header-height: 300px}}}.cd-main-header{height:200px;height:var(--cd-header-height);background:hsl(206, 21%, 24%);background:var(--cd-color-1);color:hsl(0, 0%, 100%);color:var(--color-white);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.cd-main-header h1{font-family:'Open Sans', sans-serif;font-family:var(--font-secondary);color:inherit}.cd-timeline{overflow:hidden;padding:2em 0;padding:var(--space-lg) 0;color:hsl(207, 10%, 55%);color:var(--cd-color-3);background-color:hsl(205, 38%, 93.45%);background-color:hsl(var(--cd-color-2-h), var(--cd-color-2-s), calc(var(--cd-color-2-l)*1.05));font-family:'Droid Serif', serif;font-family:var(--font-primary)}.cd-timeline h2{font-family:'Open Sans', sans-serif;font-family:var(--font-secondary);font-weight:700}.cd-timeline__container{position:relative;padding:1.25em 0;padding:var(--space-md) 0}.cd-timeline__container::before{content:'';position:absolute;top:0;left:18px;height:100%;width:4px;background:hsl(205, 38%, 89%);background:var(--cd-color-2)}@media (min-width: 64rem){.cd-timeline__container::before{left:50%;-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%)}}.cd-timeline__block{display:-ms-flexbox;display:flex;position:relative;z-index:1;margin-bottom:2em;margin-bottom:var(--space-lg)}.cd-timeline__block:last-child{margin-bottom:0}@media (min-width: 64rem){.cd-timeline__block:nth-child(even){-ms-flex-direction:row-reverse;flex-direction:row-reverse}}.cd-timeline__img{display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center;-ms-flex-negative:0;flex-shrink:0;width:40px;height:40px;border-radius:50%;box-shadow:0 0 0 4px hsl(0, 0%, 100%),inset 0 2px 0 rgba(0,0,0,0.08),0 3px 0 4px rgba(0,0,0,0.05);box-shadow:0 0 0 4px var(--color-white),inset 0 2px 0 rgba(0,0,0,0.08),0 3px 0 4px rgba(0,0,0,0.05)}.cd-timeline__img img{width:24px;height:24px}@media (min-width: 64rem){.cd-timeline__img{width:60px;height:60px;-ms-flex-order:1;order:1;margin-left:calc(5% - 30px);will-change:transform}.cd-timeline__block:nth-child(even) .cd-timeline__img{margin-right:calc(5% - 30px)}}.cd-timeline__img--picture{background-color:hsl(111, 51%, 60%);background-color:var(--cd-color-4)}.cd-timeline__img--movie{background-color:hsl(356, 53%, 49%);background-color:var(--cd-color-5)}.cd-timeline__img--location{background-color:hsl(47, 85%, 61%);background-color:var(--cd-color-6)}.cd-timeline__content{-ms-flex-positive:1;flex-grow:1;position:relative;margin-left:1.25em;margin-left:var(--space-md);background:hsl(0, 0%, 100%);background:var(--color-white);border-radius:0.25em;border-radius:var(--radius-md);padding:1.25em;padding:var(--space-md);box-shadow:0 3px 0 hsl(205, 38%, 89%);box-shadow:0 3px 0 var(--cd-color-2)}.cd-timeline__content::before{content:'';position:absolute;top:16px;right:100%;width:0;height:0;border:7px solid transparent;border-right-color:hsl(0, 0%, 100%);border-right-color:var(--color-white)}.cd-timeline__content h2{color:hsl(206, 21%, 24%);color:var(--cd-color-1)}@media (min-width: 64rem){.cd-timeline__content{width:45%;-ms-flex-positive:0;flex-grow:0;will-change:transform;margin:0;font-size:0.8em;--line-height-multiplier: 1.2}.cd-timeline__content::before{top:24px}.cd-timeline__block:nth-child(odd) .cd-timeline__content::before{right:auto;left:100%;width:0;height:0;border:7px solid transparent;border-left-color:hsl(0, 0%, 100%);border-left-color:var(--color-white)}}.cd-timeline__date{color:hsla(207, 10%, 55%, 0.7);color:hsla(var(--cd-color-3-h), var(--cd-color-3-s), var(--cd-color-3-l), 0.7)}@media (min-width: 64rem){.cd-timeline__date{position:absolute;width:100%;left:120%;top:20px}.cd-timeline__block:nth-child(even) .cd-timeline__date{left:auto;right:120%;text-align:right}}@media (min-width: 64rem){.cd-timeline__img--hidden,.cd-timeline__content--hidden{visibility:hidden}.cd-timeline__img--bounce-in{-webkit-animation:cd-bounce-1 0.6s;animation:cd-bounce-1 0.6s}.cd-timeline__content--bounce-in{-webkit-animation:cd-bounce-2 0.6s;animation:cd-bounce-2 0.6s}.cd-timeline__block:nth-child(even) .cd-timeline__content--bounce-in{-webkit-animation-name:cd-bounce-2-inverse;animation-name:cd-bounce-2-inverse}}@-webkit-keyframes cd-bounce-1{0%{opacity:0;-webkit-transform:scale(0.5);transform:scale(0.5)}60%{opacity:1;-webkit-transform:scale(1.2);transform:scale(1.2)}100%{-webkit-transform:scale(1);transform:scale(1)}}@keyframes cd-bounce-1{0%{opacity:0;-webkit-transform:scale(0.5);transform:scale(0.5)}60%{opacity:1;-webkit-transform:scale(1.2);transform:scale(1.2)}100%{-webkit-transform:scale(1);transform:scale(1)}}@-webkit-keyframes cd-bounce-2{0%{opacity:0;-webkit-transform:translateX(-100px);transform:translateX(-100px)}60%{opacity:1;-webkit-transform:translateX(20px);transform:translateX(20px)}100%{-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes cd-bounce-2{0%{opacity:0;-webkit-transform:translateX(-100px);transform:translateX(-100px)}60%{opacity:1;-webkit-transform:translateX(20px);transform:translateX(20px)}100%{-webkit-transform:translateX(0);transform:translateX(0)}}@-webkit-keyframes cd-bounce-2-inverse{0%{opacity:0;-webkit-transform:translateX(100px);transform:translateX(100px)}60%{opacity:1;-webkit-transform:translateX(-20px);transform:translateX(-20px)}100%{-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes cd-bounce-2-inverse{0%{opacity:0;-webkit-transform:translateX(100px);transform:translateX(100px)}60%{opacity:1;-webkit-transform:translateX(-20px);transform:translateX(-20px)}100%{-webkit-transform:translateX(0);transform:translateX(0)}} +@import url("https://fonts.googleapis.com/css?family=Droid+Serif|Open+Sans:400,700"); +*, *::after, *::before { + box-sizing: inherit +} + +html { + box-sizing: border-box +} + +body { + background-color: hsl(0, 0%, 100%); + background-color: var(--color-bg, white) +} + +article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section, main, form legend { + display: block +} + +ol, ul { + list-style: none +} + +blockquote, q { + quotes: none +} + +button, input, textarea, select { + margin: 0 +} + +.btn, .form-control, .link, .reset { + background-color: transparent; + padding: 0; + border: 0; + border-radius: 0; + color: inherit; + line-height: inherit; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none +} + +select.form-control::-ms-expand { + display: none +} + +textarea { + resize: vertical; + overflow: auto; + vertical-align: top +} + +input::-ms-clear { + display: none +} + +table { + border-collapse: collapse; + border-spacing: 0 +} + +img, video, svg { + max-width: 100% +} + +[data-theme] { + background-color: hsl(0, 0%, 100%); + background-color: var(--color-bg, #fff); + color: hsl(240, 4%, 20%); + color: var(--color-contrast-high, #313135) +} + +:root { + --space-unit: 1em; + --space-xxxxs: calc(0.125*var(--space-unit)); + --space-xxxs: calc(0.25*var(--space-unit)); + --space-xxs: calc(0.375*var(--space-unit)); + --space-xs: calc(0.5*var(--space-unit)); + --space-sm: calc(0.75*var(--space-unit)); + --space-md: calc(1.25*var(--space-unit)); + --space-lg: calc(2*var(--space-unit)); + --space-xl: calc(3.25*var(--space-unit)); + --space-xxl: calc(5.25*var(--space-unit)); + --space-xxxl: calc(8.5*var(--space-unit)); + --space-xxxxl: calc(13.75*var(--space-unit)); + --component-padding: var(--space-md) +} + +:root { + --max-width-xxs: 32rem; + --max-width-xs: 38rem; + --max-width-sm: 48rem; + --max-width-md: 64rem; + --max-width-lg: 80rem; + --max-width-xl: 90rem; + --max-width-xxl: 120rem +} + +.container { + width: calc(100% - 1.25em); + width: calc(100% - 2*var(--component-padding)); + margin-left: auto; + margin-right: auto +} + +.max-width-xxs { + max-width: 32rem; + max-width: var(--max-width-xxs) +} + +.max-width-xs { + max-width: 38rem; + max-width: var(--max-width-xs) +} + +.max-width-sm { + max-width: 48rem; + max-width: var(--max-width-sm) +} + +.max-width-md { + max-width: 64rem; + max-width: var(--max-width-md) +} + +.max-width-lg { + max-width: 80rem; + max-width: var(--max-width-lg) +} + +.max-width-xl { + max-width: 90rem; + max-width: var(--max-width-xl) +} + +.max-width-xxl { + max-width: 120rem; + max-width: var(--max-width-xxl) +} + +.max-width-adaptive-sm { + max-width: 38rem; + max-width: var(--max-width-xs) +} + +@media (min-width: 64rem) { + .max-width-adaptive-sm { + max-width: 48rem; + max-width: var(--max-width-sm) + } +} + +.max-width-adaptive-md { + max-width: 38rem; + max-width: var(--max-width-xs) +} + +@media (min-width: 64rem) { + .max-width-adaptive-md { + max-width: 64rem; + max-width: var(--max-width-md) + } +} + +.max-width-adaptive, .max-width-adaptive-lg { + max-width: 38rem; + max-width: var(--max-width-xs) +} + +@media (min-width: 64rem) { + .max-width-adaptive, .max-width-adaptive-lg { + max-width: 64rem; + max-width: var(--max-width-md) + } +} + +@media (min-width: 90rem) { + .max-width-adaptive, .max-width-adaptive-lg { + max-width: 80rem; + max-width: var(--max-width-lg) + } +} + +.max-width-adaptive-xl { + max-width: 38rem; + max-width: var(--max-width-xs) +} + +@media (min-width: 64rem) { + .max-width-adaptive-xl { + max-width: 64rem; + max-width: var(--max-width-md) + } +} + +@media (min-width: 90rem) { + .max-width-adaptive-xl { + max-width: 90rem; + max-width: var(--max-width-xl) + } +} + +.grid { + --grid-gap: 0px; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap +} + +.grid>* { + -ms-flex-preferred-size: 100%; + flex-basis: 100% +} + +[class*="grid-gap"] { + margin-bottom: 1em * -1; + margin-bottom: calc(var(--grid-gap, 1em)*-1); + margin-right: 1em * -1; + margin-right: calc(var(--grid-gap, 1em)*-1) +} + +[class*="grid-gap"]>* { + margin-bottom: 1em; + margin-bottom: var(--grid-gap, 1em); + margin-right: 1em; + margin-right: var(--grid-gap, 1em) +} + +.grid-gap-xxxxs { + --grid-gap: var(--space-xxxxs) +} + +.grid-gap-xxxs { + --grid-gap: var(--space-xxxs) +} + +.grid-gap-xxs { + --grid-gap: var(--space-xxs) +} + +.grid-gap-xs { + --grid-gap: var(--space-xs) +} + +.grid-gap-sm { + --grid-gap: var(--space-sm) +} + +.grid-gap-md { + --grid-gap: var(--space-md) +} + +.grid-gap-lg { + --grid-gap: var(--space-lg) +} + +.grid-gap-xl { + --grid-gap: var(--space-xl) +} + +.grid-gap-xxl { + --grid-gap: var(--space-xxl) +} + +.grid-gap-xxxl { + --grid-gap: var(--space-xxxl) +} + +.grid-gap-xxxxl { + --grid-gap: var(--space-xxxxl) +} + +.col { + -ms-flex-positive: 1; + flex-grow: 1; + -ms-flex-preferred-size: 0; + flex-basis: 0; + max-width: 100% +} + +.col-1 { + -ms-flex-preferred-size: calc(8.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(8.33% - 0.01px - 1em); + flex-basis: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(8.33% - 0.01px - 1em); + max-width: calc(8.33% - 0.01px - var(--grid-gap, 1em)) +} + +.col-2 { + -ms-flex-preferred-size: calc(16.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(16.66% - 0.01px - 1em); + flex-basis: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(16.66% - 0.01px - 1em); + max-width: calc(16.66% - 0.01px - var(--grid-gap, 1em)) +} + +.col-3 { + -ms-flex-preferred-size: calc(25% - 0.01px - 1em); + -ms-flex-preferred-size: calc(25% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(25% - 0.01px - 1em); + flex-basis: calc(25% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(25% - 0.01px - 1em); + max-width: calc(25% - 0.01px - var(--grid-gap, 1em)) +} + +.col-4 { + -ms-flex-preferred-size: calc(33.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(33.33% - 0.01px - 1em); + flex-basis: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(33.33% - 0.01px - 1em); + max-width: calc(33.33% - 0.01px - var(--grid-gap, 1em)) +} + +.col-5 { + -ms-flex-preferred-size: calc(41.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(41.66% - 0.01px - 1em); + flex-basis: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(41.66% - 0.01px - 1em); + max-width: calc(41.66% - 0.01px - var(--grid-gap, 1em)) +} + +.col-6 { + -ms-flex-preferred-size: calc(50% - 0.01px - 1em); + -ms-flex-preferred-size: calc(50% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(50% - 0.01px - 1em); + flex-basis: calc(50% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(50% - 0.01px - 1em); + max-width: calc(50% - 0.01px - var(--grid-gap, 1em)) +} + +.col-7 { + -ms-flex-preferred-size: calc(58.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(58.33% - 0.01px - 1em); + flex-basis: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(58.33% - 0.01px - 1em); + max-width: calc(58.33% - 0.01px - var(--grid-gap, 1em)) +} + +.col-8 { + -ms-flex-preferred-size: calc(66.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(66.66% - 0.01px - 1em); + flex-basis: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(66.66% - 0.01px - 1em); + max-width: calc(66.66% - 0.01px - var(--grid-gap, 1em)) +} + +.col-9 { + -ms-flex-preferred-size: calc(75% - 0.01px - 1em); + -ms-flex-preferred-size: calc(75% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(75% - 0.01px - 1em); + flex-basis: calc(75% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(75% - 0.01px - 1em); + max-width: calc(75% - 0.01px - var(--grid-gap, 1em)) +} + +.col-10 { + -ms-flex-preferred-size: calc(83.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(83.33% - 0.01px - 1em); + flex-basis: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(83.33% - 0.01px - 1em); + max-width: calc(83.33% - 0.01px - var(--grid-gap, 1em)) +} + +.col-11 { + -ms-flex-preferred-size: calc(91.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(91.66% - 0.01px - 1em); + flex-basis: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(91.66% - 0.01px - 1em); + max-width: calc(91.66% - 0.01px - var(--grid-gap, 1em)) +} + +.col-12 { + -ms-flex-preferred-size: calc(100% - 0.01px - 1em); + -ms-flex-preferred-size: calc(100% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(100% - 0.01px - 1em); + flex-basis: calc(100% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(100% - 0.01px - 1em); + max-width: calc(100% - 0.01px - var(--grid-gap, 1em)) +} + +@media (min-width: 32rem) { + .col\@xs { + -ms-flex-positive: 1; + flex-grow: 1; + -ms-flex-preferred-size: 0; + flex-basis: 0; + max-width: 100% + } + .col-1\@xs { + -ms-flex-preferred-size: calc(8.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(8.33% - 0.01px - 1em); + flex-basis: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(8.33% - 0.01px - 1em); + max-width: calc(8.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-2\@xs { + -ms-flex-preferred-size: calc(16.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(16.66% - 0.01px - 1em); + flex-basis: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(16.66% - 0.01px - 1em); + max-width: calc(16.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-3\@xs { + -ms-flex-preferred-size: calc(25% - 0.01px - 1em); + -ms-flex-preferred-size: calc(25% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(25% - 0.01px - 1em); + flex-basis: calc(25% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(25% - 0.01px - 1em); + max-width: calc(25% - 0.01px - var(--grid-gap, 1em)) + } + .col-4\@xs { + -ms-flex-preferred-size: calc(33.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(33.33% - 0.01px - 1em); + flex-basis: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(33.33% - 0.01px - 1em); + max-width: calc(33.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-5\@xs { + -ms-flex-preferred-size: calc(41.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(41.66% - 0.01px - 1em); + flex-basis: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(41.66% - 0.01px - 1em); + max-width: calc(41.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-6\@xs { + -ms-flex-preferred-size: calc(50% - 0.01px - 1em); + -ms-flex-preferred-size: calc(50% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(50% - 0.01px - 1em); + flex-basis: calc(50% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(50% - 0.01px - 1em); + max-width: calc(50% - 0.01px - var(--grid-gap, 1em)) + } + .col-7\@xs { + -ms-flex-preferred-size: calc(58.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(58.33% - 0.01px - 1em); + flex-basis: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(58.33% - 0.01px - 1em); + max-width: calc(58.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-8\@xs { + -ms-flex-preferred-size: calc(66.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(66.66% - 0.01px - 1em); + flex-basis: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(66.66% - 0.01px - 1em); + max-width: calc(66.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-9\@xs { + -ms-flex-preferred-size: calc(75% - 0.01px - 1em); + -ms-flex-preferred-size: calc(75% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(75% - 0.01px - 1em); + flex-basis: calc(75% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(75% - 0.01px - 1em); + max-width: calc(75% - 0.01px - var(--grid-gap, 1em)) + } + .col-10\@xs { + -ms-flex-preferred-size: calc(83.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(83.33% - 0.01px - 1em); + flex-basis: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(83.33% - 0.01px - 1em); + max-width: calc(83.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-11\@xs { + -ms-flex-preferred-size: calc(91.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(91.66% - 0.01px - 1em); + flex-basis: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(91.66% - 0.01px - 1em); + max-width: calc(91.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-12\@xs { + -ms-flex-preferred-size: calc(100% - 0.01px - 1em); + -ms-flex-preferred-size: calc(100% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(100% - 0.01px - 1em); + flex-basis: calc(100% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(100% - 0.01px - 1em); + max-width: calc(100% - 0.01px - var(--grid-gap, 1em)) + } +} + +@media (min-width: 48rem) { + .col\@sm { + -ms-flex-positive: 1; + flex-grow: 1; + -ms-flex-preferred-size: 0; + flex-basis: 0; + max-width: 100% + } + .col-1\@sm { + -ms-flex-preferred-size: calc(8.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(8.33% - 0.01px - 1em); + flex-basis: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(8.33% - 0.01px - 1em); + max-width: calc(8.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-2\@sm { + -ms-flex-preferred-size: calc(16.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(16.66% - 0.01px - 1em); + flex-basis: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(16.66% - 0.01px - 1em); + max-width: calc(16.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-3\@sm { + -ms-flex-preferred-size: calc(25% - 0.01px - 1em); + -ms-flex-preferred-size: calc(25% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(25% - 0.01px - 1em); + flex-basis: calc(25% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(25% - 0.01px - 1em); + max-width: calc(25% - 0.01px - var(--grid-gap, 1em)) + } + .col-4\@sm { + -ms-flex-preferred-size: calc(33.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(33.33% - 0.01px - 1em); + flex-basis: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(33.33% - 0.01px - 1em); + max-width: calc(33.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-5\@sm { + -ms-flex-preferred-size: calc(41.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(41.66% - 0.01px - 1em); + flex-basis: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(41.66% - 0.01px - 1em); + max-width: calc(41.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-6\@sm { + -ms-flex-preferred-size: calc(50% - 0.01px - 1em); + -ms-flex-preferred-size: calc(50% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(50% - 0.01px - 1em); + flex-basis: calc(50% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(50% - 0.01px - 1em); + max-width: calc(50% - 0.01px - var(--grid-gap, 1em)) + } + .col-7\@sm { + -ms-flex-preferred-size: calc(58.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(58.33% - 0.01px - 1em); + flex-basis: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(58.33% - 0.01px - 1em); + max-width: calc(58.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-8\@sm { + -ms-flex-preferred-size: calc(66.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(66.66% - 0.01px - 1em); + flex-basis: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(66.66% - 0.01px - 1em); + max-width: calc(66.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-9\@sm { + -ms-flex-preferred-size: calc(75% - 0.01px - 1em); + -ms-flex-preferred-size: calc(75% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(75% - 0.01px - 1em); + flex-basis: calc(75% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(75% - 0.01px - 1em); + max-width: calc(75% - 0.01px - var(--grid-gap, 1em)) + } + .col-10\@sm { + -ms-flex-preferred-size: calc(83.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(83.33% - 0.01px - 1em); + flex-basis: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(83.33% - 0.01px - 1em); + max-width: calc(83.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-11\@sm { + -ms-flex-preferred-size: calc(91.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(91.66% - 0.01px - 1em); + flex-basis: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(91.66% - 0.01px - 1em); + max-width: calc(91.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-12\@sm { + -ms-flex-preferred-size: calc(100% - 0.01px - 1em); + -ms-flex-preferred-size: calc(100% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(100% - 0.01px - 1em); + flex-basis: calc(100% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(100% - 0.01px - 1em); + max-width: calc(100% - 0.01px - var(--grid-gap, 1em)) + } +} + +@media (min-width: 64rem) { + .col\@md { + -ms-flex-positive: 1; + flex-grow: 1; + -ms-flex-preferred-size: 0; + flex-basis: 0; + max-width: 100% + } + .col-1\@md { + -ms-flex-preferred-size: calc(8.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(8.33% - 0.01px - 1em); + flex-basis: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(8.33% - 0.01px - 1em); + max-width: calc(8.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-2\@md { + -ms-flex-preferred-size: calc(16.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(16.66% - 0.01px - 1em); + flex-basis: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(16.66% - 0.01px - 1em); + max-width: calc(16.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-3\@md { + -ms-flex-preferred-size: calc(25% - 0.01px - 1em); + -ms-flex-preferred-size: calc(25% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(25% - 0.01px - 1em); + flex-basis: calc(25% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(25% - 0.01px - 1em); + max-width: calc(25% - 0.01px - var(--grid-gap, 1em)) + } + .col-4\@md { + -ms-flex-preferred-size: calc(33.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(33.33% - 0.01px - 1em); + flex-basis: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(33.33% - 0.01px - 1em); + max-width: calc(33.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-5\@md { + -ms-flex-preferred-size: calc(41.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(41.66% - 0.01px - 1em); + flex-basis: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(41.66% - 0.01px - 1em); + max-width: calc(41.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-6\@md { + -ms-flex-preferred-size: calc(50% - 0.01px - 1em); + -ms-flex-preferred-size: calc(50% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(50% - 0.01px - 1em); + flex-basis: calc(50% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(50% - 0.01px - 1em); + max-width: calc(50% - 0.01px - var(--grid-gap, 1em)) + } + .col-7\@md { + -ms-flex-preferred-size: calc(58.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(58.33% - 0.01px - 1em); + flex-basis: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(58.33% - 0.01px - 1em); + max-width: calc(58.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-8\@md { + -ms-flex-preferred-size: calc(66.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(66.66% - 0.01px - 1em); + flex-basis: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(66.66% - 0.01px - 1em); + max-width: calc(66.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-9\@md { + -ms-flex-preferred-size: calc(75% - 0.01px - 1em); + -ms-flex-preferred-size: calc(75% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(75% - 0.01px - 1em); + flex-basis: calc(75% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(75% - 0.01px - 1em); + max-width: calc(75% - 0.01px - var(--grid-gap, 1em)) + } + .col-10\@md { + -ms-flex-preferred-size: calc(83.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(83.33% - 0.01px - 1em); + flex-basis: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(83.33% - 0.01px - 1em); + max-width: calc(83.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-11\@md { + -ms-flex-preferred-size: calc(91.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(91.66% - 0.01px - 1em); + flex-basis: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(91.66% - 0.01px - 1em); + max-width: calc(91.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-12\@md { + -ms-flex-preferred-size: calc(100% - 0.01px - 1em); + -ms-flex-preferred-size: calc(100% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(100% - 0.01px - 1em); + flex-basis: calc(100% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(100% - 0.01px - 1em); + max-width: calc(100% - 0.01px - var(--grid-gap, 1em)) + } +} + +@media (min-width: 80rem) { + .col\@lg { + -ms-flex-positive: 1; + flex-grow: 1; + -ms-flex-preferred-size: 0; + flex-basis: 0; + max-width: 100% + } + .col-1\@lg { + -ms-flex-preferred-size: calc(8.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(8.33% - 0.01px - 1em); + flex-basis: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(8.33% - 0.01px - 1em); + max-width: calc(8.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-2\@lg { + -ms-flex-preferred-size: calc(16.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(16.66% - 0.01px - 1em); + flex-basis: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(16.66% - 0.01px - 1em); + max-width: calc(16.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-3\@lg { + -ms-flex-preferred-size: calc(25% - 0.01px - 1em); + -ms-flex-preferred-size: calc(25% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(25% - 0.01px - 1em); + flex-basis: calc(25% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(25% - 0.01px - 1em); + max-width: calc(25% - 0.01px - var(--grid-gap, 1em)) + } + .col-4\@lg { + -ms-flex-preferred-size: calc(33.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(33.33% - 0.01px - 1em); + flex-basis: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(33.33% - 0.01px - 1em); + max-width: calc(33.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-5\@lg { + -ms-flex-preferred-size: calc(41.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(41.66% - 0.01px - 1em); + flex-basis: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(41.66% - 0.01px - 1em); + max-width: calc(41.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-6\@lg { + -ms-flex-preferred-size: calc(50% - 0.01px - 1em); + -ms-flex-preferred-size: calc(50% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(50% - 0.01px - 1em); + flex-basis: calc(50% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(50% - 0.01px - 1em); + max-width: calc(50% - 0.01px - var(--grid-gap, 1em)) + } + .col-7\@lg { + -ms-flex-preferred-size: calc(58.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(58.33% - 0.01px - 1em); + flex-basis: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(58.33% - 0.01px - 1em); + max-width: calc(58.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-8\@lg { + -ms-flex-preferred-size: calc(66.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(66.66% - 0.01px - 1em); + flex-basis: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(66.66% - 0.01px - 1em); + max-width: calc(66.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-9\@lg { + -ms-flex-preferred-size: calc(75% - 0.01px - 1em); + -ms-flex-preferred-size: calc(75% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(75% - 0.01px - 1em); + flex-basis: calc(75% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(75% - 0.01px - 1em); + max-width: calc(75% - 0.01px - var(--grid-gap, 1em)) + } + .col-10\@lg { + -ms-flex-preferred-size: calc(83.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(83.33% - 0.01px - 1em); + flex-basis: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(83.33% - 0.01px - 1em); + max-width: calc(83.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-11\@lg { + -ms-flex-preferred-size: calc(91.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(91.66% - 0.01px - 1em); + flex-basis: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(91.66% - 0.01px - 1em); + max-width: calc(91.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-12\@lg { + -ms-flex-preferred-size: calc(100% - 0.01px - 1em); + -ms-flex-preferred-size: calc(100% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(100% - 0.01px - 1em); + flex-basis: calc(100% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(100% - 0.01px - 1em); + max-width: calc(100% - 0.01px - var(--grid-gap, 1em)) + } +} + +@media (min-width: 90rem) { + .col\@xl { + -ms-flex-positive: 1; + flex-grow: 1; + -ms-flex-preferred-size: 0; + flex-basis: 0; + max-width: 100% + } + .col-1\@xl { + -ms-flex-preferred-size: calc(8.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(8.33% - 0.01px - 1em); + flex-basis: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(8.33% - 0.01px - 1em); + max-width: calc(8.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-2\@xl { + -ms-flex-preferred-size: calc(16.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(16.66% - 0.01px - 1em); + flex-basis: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(16.66% - 0.01px - 1em); + max-width: calc(16.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-3\@xl { + -ms-flex-preferred-size: calc(25% - 0.01px - 1em); + -ms-flex-preferred-size: calc(25% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(25% - 0.01px - 1em); + flex-basis: calc(25% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(25% - 0.01px - 1em); + max-width: calc(25% - 0.01px - var(--grid-gap, 1em)) + } + .col-4\@xl { + -ms-flex-preferred-size: calc(33.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(33.33% - 0.01px - 1em); + flex-basis: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(33.33% - 0.01px - 1em); + max-width: calc(33.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-5\@xl { + -ms-flex-preferred-size: calc(41.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(41.66% - 0.01px - 1em); + flex-basis: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(41.66% - 0.01px - 1em); + max-width: calc(41.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-6\@xl { + -ms-flex-preferred-size: calc(50% - 0.01px - 1em); + -ms-flex-preferred-size: calc(50% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(50% - 0.01px - 1em); + flex-basis: calc(50% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(50% - 0.01px - 1em); + max-width: calc(50% - 0.01px - var(--grid-gap, 1em)) + } + .col-7\@xl { + -ms-flex-preferred-size: calc(58.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(58.33% - 0.01px - 1em); + flex-basis: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(58.33% - 0.01px - 1em); + max-width: calc(58.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-8\@xl { + -ms-flex-preferred-size: calc(66.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(66.66% - 0.01px - 1em); + flex-basis: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(66.66% - 0.01px - 1em); + max-width: calc(66.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-9\@xl { + -ms-flex-preferred-size: calc(75% - 0.01px - 1em); + -ms-flex-preferred-size: calc(75% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(75% - 0.01px - 1em); + flex-basis: calc(75% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(75% - 0.01px - 1em); + max-width: calc(75% - 0.01px - var(--grid-gap, 1em)) + } + .col-10\@xl { + -ms-flex-preferred-size: calc(83.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(83.33% - 0.01px - 1em); + flex-basis: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(83.33% - 0.01px - 1em); + max-width: calc(83.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-11\@xl { + -ms-flex-preferred-size: calc(91.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(91.66% - 0.01px - 1em); + flex-basis: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(91.66% - 0.01px - 1em); + max-width: calc(91.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-12\@xl { + -ms-flex-preferred-size: calc(100% - 0.01px - 1em); + -ms-flex-preferred-size: calc(100% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(100% - 0.01px - 1em); + flex-basis: calc(100% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(100% - 0.01px - 1em); + max-width: calc(100% - 0.01px - var(--grid-gap, 1em)) + } +} + +:root { + --radius-sm: calc(var(--radius, 0.25em)/2); + --radius-md: var(--radius, 0.25em); + --radius-lg: calc(var(--radius, 0.25em)*2); + --shadow-sm: 0 1px 2px rgba(0, 0, 0, .085), 0 1px 8px rgba(0, 0, 0, .1); + --shadow-md: 0 1px 8px rgba(0, 0, 0, .1), 0 8px 24px rgba(0, 0, 0, .15); + --shadow-lg: 0 1px 8px rgba(0, 0, 0, .1), 0 16px 48px rgba(0, 0, 0, .1), 0 24px 60px rgba(0, 0, 0, .1); + --bounce: cubic-bezier(0.175, 0.885, 0.32, 1.275); + --ease-in-out: cubic-bezier(0.645, 0.045, 0.355, 1); + --ease-in: cubic-bezier(0.55, 0.055, 0.675, 0.19); + --ease-out: cubic-bezier(0.215, 0.61, 0.355, 1) +} + +:root { + --body-line-height: 1.4; + --heading-line-height: 1.2 +} + +body { + font-size: 1em; + font-size: var(--text-base-size, 1em); + font-family: 'Droid Serif', serif; + font-family: var(--font-primary, sans-serif); + color: hsl(240, 4%, 20%); + color: var(--color-contrast-high, #313135) +} + +h1, h2, h3, h4 { + color: hsl(240, 8%, 12%); + color: var(--color-contrast-higher, #1c1c21); + line-height: 1.2; + line-height: var(--heading-line-height, 1.2) +} + +.text-xxxl { + font-size: 2.48832em; + font-size: var(--text-xxxl, 2.488em) +} + +h1, .text-xxl { + font-size: 2.0736em; + font-size: var(--text-xxl, 2.074em) +} + +h2, .text-xl { + font-size: 1.728em; + font-size: var(--text-xl, 1.728em) +} + +h3, .text-lg { + font-size: 1.44em; + font-size: var(--text-lg, 1.44em) +} + +h4, .text-md { + font-size: 1.2em; + font-size: var(--text-md, 1.2em) +} + +small, .text-sm { + font-size: 0.83333em; + font-size: var(--text-sm, 0.833em) +} + +.text-xs { + font-size: 0.69444em; + font-size: var(--text-xs, 0.694em) +} + +a, .link { + color: hsl(220, 90%, 56%); + color: var(--color-primary, #2a6df4); + text-decoration: underline +} + +strong, .text-bold { + font-weight: bold +} + +s { + text-decoration: line-through +} + +u, .text-underline { + text-decoration: underline +} + +.text-component { + --component-body-line-height: calc(var(--body-line-height)*var(--line-height-multiplier, 1)); + --component-heading-line-height: calc(var(--heading-line-height)*var(--line-height-multiplier, 1)) +} + +.text-component h1, .text-component h2, .text-component h3, .text-component h4 { + line-height: 1.2; + line-height: var(--component-heading-line-height, 1.2); + margin-bottom: 0.25em; + margin-bottom: calc(var(--space-xxxs)*var(--text-vspace-multiplier, 1)) +} + +.text-component h2, .text-component h3, .text-component h4 { + margin-top: 0.75em; + margin-top: calc(var(--space-sm)*var(--text-vspace-multiplier, 1)) +} + +.text-component p, .text-component blockquote, .text-component ul li, .text-component ol li { + line-height: 1.4; + line-height: var(--component-body-line-height) +} + +.text-component ul, .text-component ol, .text-component p, .text-component blockquote, .text-component .text-component__block { + margin-bottom: 0.75em; + margin-bottom: calc(var(--space-sm)*var(--text-vspace-multiplier, 1)) +} + +.text-component ul, .text-component ol { + padding-left: 1em +} + +.text-component ul { + list-style-type: disc +} + +.text-component ol { + list-style-type: decimal +} + +.text-component img { + display: block; + margin: 0 auto +} + +.text-component figcaption { + text-align: center; + margin-top: 0.5em; + margin-top: var(--space-xs) +} + +.text-component em { + font-style: italic +} + +.text-component hr { + margin-top: 2em; + margin-top: calc(var(--space-lg)*var(--text-vspace-multiplier, 1)); + margin-bottom: 2em; + margin-bottom: calc(var(--space-lg)*var(--text-vspace-multiplier, 1)); + margin-left: auto; + margin-right: auto +} + +.text-component>*:first-child { + margin-top: 0 +} + +.text-component>*:last-child { + margin-bottom: 0 +} + +.text-component__block--full-width { + width: 100vw; + margin-left: calc(50% - 50vw) +} + +@media (min-width: 48rem) { + .text-component__block--left, .text-component__block--right { + width: 45% + } + .text-component__block--left img, .text-component__block--right img { + width: 100% + } + .text-component__block--left { + float: left; + margin-right: 0.75em; + margin-right: calc(var(--space-sm)*var(--text-vspace-multiplier, 1)) + } + .text-component__block--right { + float: right; + margin-left: 0.75em; + margin-left: calc(var(--space-sm)*var(--text-vspace-multiplier, 1)) + } +} + +@media (min-width: 90rem) { + .text-component__block--outset { + width: calc(100% + 10.5em); + width: calc(100% + 2*var(--space-xxl)) + } + .text-component__block--outset img { + width: 100% + } + .text-component__block--outset:not(.text-component__block--right) { + margin-left: -5.25em; + margin-left: calc(-1*var(--space-xxl)) + } + .text-component__block--left, .text-component__block--right { + width: 50% + } + .text-component__block--right.text-component__block--outset { + margin-right: -5.25em; + margin-right: calc(-1*var(--space-xxl)) + } +} + +:root { + --icon-xxs: 12px; + --icon-xs: 16px; + --icon-sm: 24px; + --icon-md: 32px; + --icon-lg: 48px; + --icon-xl: 64px; + --icon-xxl: 128px +} + +.icon { + display: inline-block; + color: inherit; + fill: currentColor; + height: 1em; + width: 1em; + line-height: 1; + -ms-flex-negative: 0; + flex-shrink: 0 +} + +.icon--xxs { + font-size: 12px; + font-size: var(--icon-xxs) +} + +.icon--xs { + font-size: 16px; + font-size: var(--icon-xs) +} + +.icon--sm { + font-size: 24px; + font-size: var(--icon-sm) +} + +.icon--md { + font-size: 32px; + font-size: var(--icon-md) +} + +.icon--lg { + font-size: 48px; + font-size: var(--icon-lg) +} + +.icon--xl { + font-size: 64px; + font-size: var(--icon-xl) +} + +.icon--xxl { + font-size: 128px; + font-size: var(--icon-xxl) +} + +.icon--is-spinning { + -webkit-animation: icon-spin 1s infinite linear; + animation: icon-spin 1s infinite linear +} + +@-webkit-keyframes icon-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg) + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg) + } +} + +@keyframes icon-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg) + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg) + } +} + +.icon use { + color: inherit; + fill: currentColor +} + +.btn { + position: relative; + display: -ms-inline-flexbox; + display: inline-flex; + -ms-flex-pack: center; + justify-content: center; + -ms-flex-align: center; + align-items: center; + white-space: nowrap; + text-decoration: none; + line-height: 1; + font-size: 1em; + font-size: var(--btn-font-size, 1em); + padding-top: 0.5em; + padding-top: var(--btn-padding-y, 0.5em); + padding-bottom: 0.5em; + padding-bottom: var(--btn-padding-y, 0.5em); + padding-left: 0.75em; + padding-left: var(--btn-padding-x, 0.75em); + padding-right: 0.75em; + padding-right: var(--btn-padding-x, 0.75em); + border-radius: 0.25em; + border-radius: var(--btn-radius, 0.25em) +} + +.btn--primary { + background-color: hsl(220, 90%, 56%); + background-color: var(--color-primary, #2a6df4); + color: hsl(0, 0%, 100%); + color: var(--color-white, #fff) +} + +.btn--subtle { + background-color: hsl(240, 1%, 83%); + background-color: var(--color-contrast-low, #d3d3d4); + color: hsl(240, 8%, 12%); + color: var(--color-contrast-higher, #1c1c21) +} + +.btn--accent { + background-color: hsl(355, 90%, 61%); + background-color: var(--color-accent, #f54251); + color: hsl(0, 0%, 100%); + color: var(--color-white, #fff) +} + +.btn--disabled { + cursor: not-allowed +} + +.btn--sm { + font-size: 0.8em; + font-size: var(--btn-font-size-sm, 0.8em) +} + +.btn--md { + font-size: 1.2em; + font-size: var(--btn-font-size-md, 1.2em) +} + +.btn--lg { + font-size: 1.4em; + font-size: var(--btn-font-size-lg, 1.4em) +} + +.btn--icon { + padding: 0.5em; + padding: var(--btn-padding-y, 0.5em) +} + +.form-control { + background-color: hsl(0, 0%, 100%); + background-color: var(--color-bg, #f2f2f2); + padding-top: 0.5em; + padding-top: var(--form-control-padding-y, 0.5em); + padding-bottom: 0.5em; + padding-bottom: var(--form-control-padding-y, 0.5em); + padding-left: 0.75em; + padding-left: var(--form-control-padding-x, 0.75em); + padding-right: 0.75em; + padding-right: var(--form-control-padding-x, 0.75em); + border-radius: 0.25em; + border-radius: var(--form-control-radius, 0.25em) +} + +.form-control::-webkit-input-placeholder { + color: hsl(240, 1%, 48%); + color: var(--color-contrast-medium, #79797c) +} + +.form-control::-moz-placeholder { + opacity: 1; + color: hsl(240, 1%, 48%); + color: var(--color-contrast-medium, #79797c) +} + +.form-control:-ms-input-placeholder { + color: hsl(240, 1%, 48%); + color: var(--color-contrast-medium, #79797c) +} + +.form-control:-moz-placeholder { + color: hsl(240, 1%, 48%); + color: var(--color-contrast-medium, #79797c) +} + +.form-control[disabled], .form-control[readonly] { + cursor: not-allowed +} + +.form-legend { + color: hsl(240, 8%, 12%); + color: var(--color-contrast-higher, #1c1c21); + line-height: 1.2; + font-size: 1.2em; + font-size: var(--text-md, 1.2em); + margin-bottom: 0.375em; + margin-bottom: var(--space-xxs) +} + +.form-label { + display: inline-block +} + +.form__msg-error { + background-color: hsl(355, 90%, 61%); + background-color: var(--color-error, #f54251); + color: hsl(0, 0%, 100%); + color: var(--color-white, #fff); + font-size: 0.83333em; + font-size: var(--text-sm, 0.833em); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + padding: 0.5em; + padding: var(--space-xs); + margin-top: 0.75em; + margin-top: var(--space-sm); + border-radius: 0.25em; + border-radius: var(--radius-md, 0.25em); + position: absolute; + clip: rect(1px, 1px, 1px, 1px) +} + +.form__msg-error::before { + content: ''; + position: absolute; + left: 0.75em; + left: var(--space-sm); + top: 0; + -webkit-transform: translateY(-100%); + -ms-transform: translateY(-100%); + transform: translateY(-100%); + width: 0; + height: 0; + border: 8px solid transparent; + border-bottom-color: hsl(355, 90%, 61%); + border-bottom-color: var(--color-error) +} + +.form__msg-error--is-visible { + position: relative; + clip: auto +} + +.radio-list>*, .checkbox-list>* { + position: relative; + display: -ms-flexbox; + display: flex; + -ms-flex-align: baseline; + align-items: baseline; + margin-bottom: 0.375em; + margin-bottom: var(--space-xxs) +} + +.radio-list>*:last-of-type, .checkbox-list>*:last-of-type { + margin-bottom: 0 +} + +.radio-list label, .checkbox-list label { + line-height: 1.4; + line-height: var(--body-line-height); + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none +} + +.radio-list input, .checkbox-list input { + vertical-align: top; + margin-right: 0.25em; + margin-right: var(--space-xxxs); + -ms-flex-negative: 0; + flex-shrink: 0 +} + +:root { + --zindex-header: 2; + --zindex-popover: 5; + --zindex-fixed-element: 10; + --zindex-overlay: 15 +} + +@media not all and (min-width: 32rem) { + .display\@xs { + display: none !important + } +} + +@media (min-width: 32rem) { + .hide\@xs { + display: none !important + } +} + +@media not all and (min-width: 48rem) { + .display\@sm { + display: none !important + } +} + +@media (min-width: 48rem) { + .hide\@sm { + display: none !important + } +} + +@media not all and (min-width: 64rem) { + .display\@md { + display: none !important + } +} + +@media (min-width: 64rem) { + .hide\@md { + display: none !important + } +} + +@media not all and (min-width: 80rem) { + .display\@lg { + display: none !important + } +} + +@media (min-width: 80rem) { + .hide\@lg { + display: none !important + } +} + +@media not all and (min-width: 90rem) { + .display\@xl { + display: none !important + } +} + +@media (min-width: 90rem) { + .hide\@xl { + display: none !important + } +} + +:root { + --display: block +} + +.is-visible { + display: block !important; + display: var(--display) !important +} + +.is-hidden { + display: none !important +} + +.sr-only { + position: absolute; + clip: rect(1px, 1px, 1px, 1px); + -webkit-clip-path: inset(50%); + clip-path: inset(50%); + width: 1px; + height: 1px; + overflow: hidden; + padding: 0; + border: 0; + white-space: nowrap +} + +.flex { + display: -ms-flexbox; + display: flex +} + +.inline-flex { + display: -ms-inline-flexbox; + display: inline-flex +} + +.flex-wrap { + -ms-flex-wrap: wrap; + flex-wrap: wrap +} + +.flex-column { + -ms-flex-direction: column; + flex-direction: column +} + +.flex-row { + -ms-flex-direction: row; + flex-direction: row +} + +.flex-center { + -ms-flex-pack: center; + justify-content: center; + -ms-flex-align: center; + align-items: center +} + +.justify-start { + -ms-flex-pack: start; + justify-content: flex-start +} + +.justify-end { + -ms-flex-pack: end; + justify-content: flex-end +} + +.justify-center { + -ms-flex-pack: center; + justify-content: center +} + +.justify-between { + -ms-flex-pack: justify; + justify-content: space-between +} + +.items-center { + -ms-flex-align: center; + align-items: center +} + +.items-start { + -ms-flex-align: start; + align-items: flex-start +} + +.items-end { + -ms-flex-align: end; + align-items: flex-end +} + +@media (min-width: 32rem) { + .flex-wrap\@xs { + -ms-flex-wrap: wrap; + flex-wrap: wrap + } + .flex-column\@xs { + -ms-flex-direction: column; + flex-direction: column + } + .flex-row\@xs { + -ms-flex-direction: row; + flex-direction: row + } + .flex-center\@xs { + -ms-flex-pack: center; + justify-content: center; + -ms-flex-align: center; + align-items: center + } + .justify-start\@xs { + -ms-flex-pack: start; + justify-content: flex-start + } + .justify-end\@xs { + -ms-flex-pack: end; + justify-content: flex-end + } + .justify-center\@xs { + -ms-flex-pack: center; + justify-content: center + } + .justify-between\@xs { + -ms-flex-pack: justify; + justify-content: space-between + } + .items-center\@xs { + -ms-flex-align: center; + align-items: center + } + .items-start\@xs { + -ms-flex-align: start; + align-items: flex-start + } + .items-end\@xs { + -ms-flex-align: end; + align-items: flex-end + } +} + +@media (min-width: 48rem) { + .flex-wrap\@sm { + -ms-flex-wrap: wrap; + flex-wrap: wrap + } + .flex-column\@sm { + -ms-flex-direction: column; + flex-direction: column + } + .flex-row\@sm { + -ms-flex-direction: row; + flex-direction: row + } + .flex-center\@sm { + -ms-flex-pack: center; + justify-content: center; + -ms-flex-align: center; + align-items: center + } + .justify-start\@sm { + -ms-flex-pack: start; + justify-content: flex-start + } + .justify-end\@sm { + -ms-flex-pack: end; + justify-content: flex-end + } + .justify-center\@sm { + -ms-flex-pack: center; + justify-content: center + } + .justify-between\@sm { + -ms-flex-pack: justify; + justify-content: space-between + } + .items-center\@sm { + -ms-flex-align: center; + align-items: center + } + .items-start\@sm { + -ms-flex-align: start; + align-items: flex-start + } + .items-end\@sm { + -ms-flex-align: end; + align-items: flex-end + } +} + +@media (min-width: 64rem) { + .flex-wrap\@md { + -ms-flex-wrap: wrap; + flex-wrap: wrap + } + .flex-column\@md { + -ms-flex-direction: column; + flex-direction: column + } + .flex-row\@md { + -ms-flex-direction: row; + flex-direction: row + } + .flex-center\@md { + -ms-flex-pack: center; + justify-content: center; + -ms-flex-align: center; + align-items: center + } + .justify-start\@md { + -ms-flex-pack: start; + justify-content: flex-start + } + .justify-end\@md { + -ms-flex-pack: end; + justify-content: flex-end + } + .justify-center\@md { + -ms-flex-pack: center; + justify-content: center + } + .justify-between\@md { + -ms-flex-pack: justify; + justify-content: space-between + } + .items-center\@md { + -ms-flex-align: center; + align-items: center + } + .items-start\@md { + -ms-flex-align: start; + align-items: flex-start + } + .items-end\@md { + -ms-flex-align: end; + align-items: flex-end + } +} + +@media (min-width: 80rem) { + .flex-wrap\@lg { + -ms-flex-wrap: wrap; + flex-wrap: wrap + } + .flex-column\@lg { + -ms-flex-direction: column; + flex-direction: column + } + .flex-row\@lg { + -ms-flex-direction: row; + flex-direction: row + } + .flex-center\@lg { + -ms-flex-pack: center; + justify-content: center; + -ms-flex-align: center; + align-items: center + } + .justify-start\@lg { + -ms-flex-pack: start; + justify-content: flex-start + } + .justify-end\@lg { + -ms-flex-pack: end; + justify-content: flex-end + } + .justify-center\@lg { + -ms-flex-pack: center; + justify-content: center + } + .justify-between\@lg { + -ms-flex-pack: justify; + justify-content: space-between + } + .items-center\@lg { + -ms-flex-align: center; + align-items: center + } + .items-start\@lg { + -ms-flex-align: start; + align-items: flex-start + } + .items-end\@lg { + -ms-flex-align: end; + align-items: flex-end + } +} + +@media (min-width: 90rem) { + .flex-wrap\@xl { + -ms-flex-wrap: wrap; + flex-wrap: wrap + } + .flex-column\@xl { + -ms-flex-direction: column; + flex-direction: column + } + .flex-row\@xl { + -ms-flex-direction: row; + flex-direction: row + } + .flex-center\@xl { + -ms-flex-pack: center; + justify-content: center; + -ms-flex-align: center; + align-items: center + } + .justify-start\@xl { + -ms-flex-pack: start; + justify-content: flex-start + } + .justify-end\@xl { + -ms-flex-pack: end; + justify-content: flex-end + } + .justify-center\@xl { + -ms-flex-pack: center; + justify-content: center + } + .justify-between\@xl { + -ms-flex-pack: justify; + justify-content: space-between + } + .items-center\@xl { + -ms-flex-align: center; + align-items: center + } + .items-start\@xl { + -ms-flex-align: start; + align-items: flex-start + } + .items-end\@xl { + -ms-flex-align: end; + align-items: flex-end + } +} + +.flex-grow { + -ms-flex-positive: 1; + flex-grow: 1 +} + +.flex-shrink-0 { + -ms-flex-negative: 0; + flex-shrink: 0 +} + +.flex-gap-xxxs { + margin-bottom: -0.25em; + margin-bottom: calc(-1*var(--space-xxxs)); + margin-right: -0.25em; + margin-right: calc(-1*var(--space-xxxs)) +} + +.flex-gap-xxxs>* { + margin-bottom: 0.25em; + margin-bottom: var(--space-xxxs); + margin-right: 0.25em; + margin-right: var(--space-xxxs) +} + +.flex-gap-xxs { + margin-bottom: -0.375em; + margin-bottom: calc(-1*var(--space-xxs)); + margin-right: -0.375em; + margin-right: calc(-1*var(--space-xxs)) +} + +.flex-gap-xxs>* { + margin-bottom: 0.375em; + margin-bottom: var(--space-xxs); + margin-right: 0.375em; + margin-right: var(--space-xxs) +} + +.flex-gap-xs { + margin-bottom: -0.5em; + margin-bottom: calc(-1*var(--space-xs)); + margin-right: -0.5em; + margin-right: calc(-1*var(--space-xs)) +} + +.flex-gap-xs>* { + margin-bottom: 0.5em; + margin-bottom: var(--space-xs); + margin-right: 0.5em; + margin-right: var(--space-xs) +} + +.flex-gap-sm { + margin-bottom: -0.75em; + margin-bottom: calc(-1*var(--space-sm)); + margin-right: -0.75em; + margin-right: calc(-1*var(--space-sm)) +} + +.flex-gap-sm>* { + margin-bottom: 0.75em; + margin-bottom: var(--space-sm); + margin-right: 0.75em; + margin-right: var(--space-sm) +} + +.flex-gap-md { + margin-bottom: -1.25em; + margin-bottom: calc(-1*var(--space-md)); + margin-right: -1.25em; + margin-right: calc(-1*var(--space-md)) +} + +.flex-gap-md>* { + margin-bottom: 1.25em; + margin-bottom: var(--space-md); + margin-right: 1.25em; + margin-right: var(--space-md) +} + +.flex-gap-lg { + margin-bottom: -2em; + margin-bottom: calc(-1*var(--space-lg)); + margin-right: -2em; + margin-right: calc(-1*var(--space-lg)) +} + +.flex-gap-lg>* { + margin-bottom: 2em; + margin-bottom: var(--space-lg); + margin-right: 2em; + margin-right: var(--space-lg) +} + +.flex-gap-xl { + margin-bottom: -3.25em; + margin-bottom: calc(-1*var(--space-xl)); + margin-right: -3.25em; + margin-right: calc(-1*var(--space-xl)) +} + +.flex-gap-xl>* { + margin-bottom: 3.25em; + margin-bottom: var(--space-xl); + margin-right: 3.25em; + margin-right: var(--space-xl) +} + +.flex-gap-xxl { + margin-bottom: -5.25em; + margin-bottom: calc(-1*var(--space-xxl)); + margin-right: -5.25em; + margin-right: calc(-1*var(--space-xxl)) +} + +.flex-gap-xxl>* { + margin-bottom: 5.25em; + margin-bottom: var(--space-xxl); + margin-right: 5.25em; + margin-right: var(--space-xxl) +} + +.margin-xxxxs { + margin: 0.125em; + margin: var(--space-xxxxs) +} + +.margin-xxxs { + margin: 0.25em; + margin: var(--space-xxxs) +} + +.margin-xxs { + margin: 0.375em; + margin: var(--space-xxs) +} + +.margin-xs { + margin: 0.5em; + margin: var(--space-xs) +} + +.margin-sm { + margin: 0.75em; + margin: var(--space-sm) +} + +.margin-md { + margin: 1.25em; + margin: var(--space-md) +} + +.margin-lg { + margin: 2em; + margin: var(--space-lg) +} + +.margin-xl { + margin: 3.25em; + margin: var(--space-xl) +} + +.margin-xxl { + margin: 5.25em; + margin: var(--space-xxl) +} + +.margin-xxxl { + margin: 8.5em; + margin: var(--space-xxxl) +} + +.margin-xxxxl { + margin: 13.75em; + margin: var(--space-xxxxl) +} + +.margin-auto { + margin: auto +} + +.margin-top-xxxxs { + margin-top: 0.125em; + margin-top: var(--space-xxxxs) +} + +.margin-top-xxxs { + margin-top: 0.25em; + margin-top: var(--space-xxxs) +} + +.margin-top-xxs { + margin-top: 0.375em; + margin-top: var(--space-xxs) +} + +.margin-top-xs { + margin-top: 0.5em; + margin-top: var(--space-xs) +} + +.margin-top-sm { + margin-top: 0.75em; + margin-top: var(--space-sm) +} + +.margin-top-md { + margin-top: 1.25em; + margin-top: var(--space-md) +} + +.margin-top-lg { + margin-top: 2em; + margin-top: var(--space-lg) +} + +.margin-top-xl { + margin-top: 3.25em; + margin-top: var(--space-xl) +} + +.margin-top-xxl { + margin-top: 5.25em; + margin-top: var(--space-xxl) +} + +.margin-top-xxxl { + margin-top: 8.5em; + margin-top: var(--space-xxxl) +} + +.margin-top-xxxxl { + margin-top: 13.75em; + margin-top: var(--space-xxxxl) +} + +.margin-top-auto { + margin-top: auto +} + +.margin-bottom-xxxxs { + margin-bottom: 0.125em; + margin-bottom: var(--space-xxxxs) +} + +.margin-bottom-xxxs { + margin-bottom: 0.25em; + margin-bottom: var(--space-xxxs) +} + +.margin-bottom-xxs { + margin-bottom: 0.375em; + margin-bottom: var(--space-xxs) +} + +.margin-bottom-xs { + margin-bottom: 0.5em; + margin-bottom: var(--space-xs) +} + +.margin-bottom-sm { + margin-bottom: 0.75em; + margin-bottom: var(--space-sm) +} + +.margin-bottom-md { + margin-bottom: 1.25em; + margin-bottom: var(--space-md) +} + +.margin-bottom-lg { + margin-bottom: 2em; + margin-bottom: var(--space-lg) +} + +.margin-bottom-xl { + margin-bottom: 3.25em; + margin-bottom: var(--space-xl) +} + +.margin-bottom-xxl { + margin-bottom: 5.25em; + margin-bottom: var(--space-xxl) +} + +.margin-bottom-xxxl { + margin-bottom: 8.5em; + margin-bottom: var(--space-xxxl) +} + +.margin-bottom-xxxxl { + margin-bottom: 13.75em; + margin-bottom: var(--space-xxxxl) +} + +.margin-bottom-auto { + margin-bottom: auto +} + +.margin-right-xxxxs { + margin-right: 0.125em; + margin-right: var(--space-xxxxs) +} + +.margin-right-xxxs { + margin-right: 0.25em; + margin-right: var(--space-xxxs) +} + +.margin-right-xxs { + margin-right: 0.375em; + margin-right: var(--space-xxs) +} + +.margin-right-xs { + margin-right: 0.5em; + margin-right: var(--space-xs) +} + +.margin-right-sm { + margin-right: 0.75em; + margin-right: var(--space-sm) +} + +.margin-right-md { + margin-right: 1.25em; + margin-right: var(--space-md) +} + +.margin-right-lg { + margin-right: 2em; + margin-right: var(--space-lg) +} + +.margin-right-xl { + margin-right: 3.25em; + margin-right: var(--space-xl) +} + +.margin-right-xxl { + margin-right: 5.25em; + margin-right: var(--space-xxl) +} + +.margin-right-xxxl { + margin-right: 8.5em; + margin-right: var(--space-xxxl) +} + +.margin-right-xxxxl { + margin-right: 13.75em; + margin-right: var(--space-xxxxl) +} + +.margin-right-auto { + margin-right: auto +} + +.margin-left-xxxxs { + margin-left: 0.125em; + margin-left: var(--space-xxxxs) +} + +.margin-left-xxxs { + margin-left: 0.25em; + margin-left: var(--space-xxxs) +} + +.margin-left-xxs { + margin-left: 0.375em; + margin-left: var(--space-xxs) +} + +.margin-left-xs { + margin-left: 0.5em; + margin-left: var(--space-xs) +} + +.margin-left-sm { + margin-left: 0.75em; + margin-left: var(--space-sm) +} + +.margin-left-md { + margin-left: 1.25em; + margin-left: var(--space-md) +} + +.margin-left-lg { + margin-left: 2em; + margin-left: var(--space-lg) +} + +.margin-left-xl { + margin-left: 3.25em; + margin-left: var(--space-xl) +} + +.margin-left-xxl { + margin-left: 5.25em; + margin-left: var(--space-xxl) +} + +.margin-left-xxxl { + margin-left: 8.5em; + margin-left: var(--space-xxxl) +} + +.margin-left-xxxxl { + margin-left: 13.75em; + margin-left: var(--space-xxxxl) +} + +.margin-left-auto { + margin-left: auto +} + +.margin-x-xxxxs { + margin-left: 0.125em; + margin-left: var(--space-xxxxs); + margin-right: 0.125em; + margin-right: var(--space-xxxxs) +} + +.margin-x-xxxs { + margin-left: 0.25em; + margin-left: var(--space-xxxs); + margin-right: 0.25em; + margin-right: var(--space-xxxs) +} + +.margin-x-xxs { + margin-left: 0.375em; + margin-left: var(--space-xxs); + margin-right: 0.375em; + margin-right: var(--space-xxs) +} + +.margin-x-xs { + margin-left: 0.5em; + margin-left: var(--space-xs); + margin-right: 0.5em; + margin-right: var(--space-xs) +} + +.margin-x-sm { + margin-left: 0.75em; + margin-left: var(--space-sm); + margin-right: 0.75em; + margin-right: var(--space-sm) +} + +.margin-x-md { + margin-left: 1.25em; + margin-left: var(--space-md); + margin-right: 1.25em; + margin-right: var(--space-md) +} + +.margin-x-lg { + margin-left: 2em; + margin-left: var(--space-lg); + margin-right: 2em; + margin-right: var(--space-lg) +} + +.margin-x-xl { + margin-left: 3.25em; + margin-left: var(--space-xl); + margin-right: 3.25em; + margin-right: var(--space-xl) +} + +.margin-x-xxl { + margin-left: 5.25em; + margin-left: var(--space-xxl); + margin-right: 5.25em; + margin-right: var(--space-xxl) +} + +.margin-x-xxxl { + margin-left: 8.5em; + margin-left: var(--space-xxxl); + margin-right: 8.5em; + margin-right: var(--space-xxxl) +} + +.margin-x-xxxxl { + margin-left: 13.75em; + margin-left: var(--space-xxxxl); + margin-right: 13.75em; + margin-right: var(--space-xxxxl) +} + +.margin-x-auto { + margin-left: auto; + margin-right: auto +} + +.margin-y-xxxxs { + margin-top: 0.125em; + margin-top: var(--space-xxxxs); + margin-bottom: 0.125em; + margin-bottom: var(--space-xxxxs) +} + +.margin-y-xxxs { + margin-top: 0.25em; + margin-top: var(--space-xxxs); + margin-bottom: 0.25em; + margin-bottom: var(--space-xxxs) +} + +.margin-y-xxs { + margin-top: 0.375em; + margin-top: var(--space-xxs); + margin-bottom: 0.375em; + margin-bottom: var(--space-xxs) +} + +.margin-y-xs { + margin-top: 0.5em; + margin-top: var(--space-xs); + margin-bottom: 0.5em; + margin-bottom: var(--space-xs) +} + +.margin-y-sm { + margin-top: 0.75em; + margin-top: var(--space-sm); + margin-bottom: 0.75em; + margin-bottom: var(--space-sm) +} + +.margin-y-md { + margin-top: 1.25em; + margin-top: var(--space-md); + margin-bottom: 1.25em; + margin-bottom: var(--space-md) +} + +.margin-y-lg { + margin-top: 2em; + margin-top: var(--space-lg); + margin-bottom: 2em; + margin-bottom: var(--space-lg) +} + +.margin-y-xl { + margin-top: 3.25em; + margin-top: var(--space-xl); + margin-bottom: 3.25em; + margin-bottom: var(--space-xl) +} + +.margin-y-xxl { + margin-top: 5.25em; + margin-top: var(--space-xxl); + margin-bottom: 5.25em; + margin-bottom: var(--space-xxl) +} + +.margin-y-xxxl { + margin-top: 8.5em; + margin-top: var(--space-xxxl); + margin-bottom: 8.5em; + margin-bottom: var(--space-xxxl) +} + +.margin-y-xxxxl { + margin-top: 13.75em; + margin-top: var(--space-xxxxl); + margin-bottom: 13.75em; + margin-bottom: var(--space-xxxxl) +} + +.margin-y-auto { + margin-top: auto; + margin-bottom: auto +} + +@media not all and (min-width: 32rem) { + .has-margin\@xs { + margin: 0 !important + } +} + +@media not all and (min-width: 48rem) { + .has-margin\@sm { + margin: 0 !important + } +} + +@media not all and (min-width: 64rem) { + .has-margin\@md { + margin: 0 !important + } +} + +@media not all and (min-width: 80rem) { + .has-margin\@lg { + margin: 0 !important + } +} + +@media not all and (min-width: 90rem) { + .has-margin\@xl { + margin: 0 !important + } +} + +.padding-md { + padding: 1.25em; + padding: var(--space-md) +} + +.padding-xxxxs { + padding: 0.125em; + padding: var(--space-xxxxs) +} + +.padding-xxxs { + padding: 0.25em; + padding: var(--space-xxxs) +} + +.padding-xxs { + padding: 0.375em; + padding: var(--space-xxs) +} + +.padding-xs { + padding: 0.5em; + padding: var(--space-xs) +} + +.padding-sm { + padding: 0.75em; + padding: var(--space-sm) +} + +.padding-lg { + padding: 2em; + padding: var(--space-lg) +} + +.padding-xl { + padding: 3.25em; + padding: var(--space-xl) +} + +.padding-xxl { + padding: 5.25em; + padding: var(--space-xxl) +} + +.padding-xxxl { + padding: 8.5em; + padding: var(--space-xxxl) +} + +.padding-xxxxl { + padding: 13.75em; + padding: var(--space-xxxxl) +} + +.padding-component { + padding: 1.25em; + padding: var(--component-padding) +} + +.padding-top-md { + padding-top: 1.25em; + padding-top: var(--space-md) +} + +.padding-top-xxxxs { + padding-top: 0.125em; + padding-top: var(--space-xxxxs) +} + +.padding-top-xxxs { + padding-top: 0.25em; + padding-top: var(--space-xxxs) +} + +.padding-top-xxs { + padding-top: 0.375em; + padding-top: var(--space-xxs) +} + +.padding-top-xs { + padding-top: 0.5em; + padding-top: var(--space-xs) +} + +.padding-top-sm { + padding-top: 0.75em; + padding-top: var(--space-sm) +} + +.padding-top-lg { + padding-top: 2em; + padding-top: var(--space-lg) +} + +.padding-top-xl { + padding-top: 3.25em; + padding-top: var(--space-xl) +} + +.padding-top-xxl { + padding-top: 5.25em; + padding-top: var(--space-xxl) +} + +.padding-top-xxxl { + padding-top: 8.5em; + padding-top: var(--space-xxxl) +} + +.padding-top-xxxxl { + padding-top: 13.75em; + padding-top: var(--space-xxxxl) +} + +.padding-top-component { + padding-top: 1.25em; + padding-top: var(--component-padding) +} + +.padding-bottom-md { + padding-bottom: 1.25em; + padding-bottom: var(--space-md) +} + +.padding-bottom-xxxxs { + padding-bottom: 0.125em; + padding-bottom: var(--space-xxxxs) +} + +.padding-bottom-xxxs { + padding-bottom: 0.25em; + padding-bottom: var(--space-xxxs) +} + +.padding-bottom-xxs { + padding-bottom: 0.375em; + padding-bottom: var(--space-xxs) +} + +.padding-bottom-xs { + padding-bottom: 0.5em; + padding-bottom: var(--space-xs) +} + +.padding-bottom-sm { + padding-bottom: 0.75em; + padding-bottom: var(--space-sm) +} + +.padding-bottom-lg { + padding-bottom: 2em; + padding-bottom: var(--space-lg) +} + +.padding-bottom-xl { + padding-bottom: 3.25em; + padding-bottom: var(--space-xl) +} + +.padding-bottom-xxl { + padding-bottom: 5.25em; + padding-bottom: var(--space-xxl) +} + +.padding-bottom-xxxl { + padding-bottom: 8.5em; + padding-bottom: var(--space-xxxl) +} + +.padding-bottom-xxxxl { + padding-bottom: 13.75em; + padding-bottom: var(--space-xxxxl) +} + +.padding-bottom-component { + padding-bottom: 1.25em; + padding-bottom: var(--component-padding) +} + +.padding-right-md { + padding-right: 1.25em; + padding-right: var(--space-md) +} + +.padding-right-xxxxs { + padding-right: 0.125em; + padding-right: var(--space-xxxxs) +} + +.padding-right-xxxs { + padding-right: 0.25em; + padding-right: var(--space-xxxs) +} + +.padding-right-xxs { + padding-right: 0.375em; + padding-right: var(--space-xxs) +} + +.padding-right-xs { + padding-right: 0.5em; + padding-right: var(--space-xs) +} + +.padding-right-sm { + padding-right: 0.75em; + padding-right: var(--space-sm) +} + +.padding-right-lg { + padding-right: 2em; + padding-right: var(--space-lg) +} + +.padding-right-xl { + padding-right: 3.25em; + padding-right: var(--space-xl) +} + +.padding-right-xxl { + padding-right: 5.25em; + padding-right: var(--space-xxl) +} + +.padding-right-xxxl { + padding-right: 8.5em; + padding-right: var(--space-xxxl) +} + +.padding-right-xxxxl { + padding-right: 13.75em; + padding-right: var(--space-xxxxl) +} + +.padding-right-component { + padding-right: 1.25em; + padding-right: var(--component-padding) +} + +.padding-left-md { + padding-left: 1.25em; + padding-left: var(--space-md) +} + +.padding-left-xxxxs { + padding-left: 0.125em; + padding-left: var(--space-xxxxs) +} + +.padding-left-xxxs { + padding-left: 0.25em; + padding-left: var(--space-xxxs) +} + +.padding-left-xxs { + padding-left: 0.375em; + padding-left: var(--space-xxs) +} + +.padding-left-xs { + padding-left: 0.5em; + padding-left: var(--space-xs) +} + +.padding-left-sm { + padding-left: 0.75em; + padding-left: var(--space-sm) +} + +.padding-left-lg { + padding-left: 2em; + padding-left: var(--space-lg) +} + +.padding-left-xl { + padding-left: 3.25em; + padding-left: var(--space-xl) +} + +.padding-left-xxl { + padding-left: 5.25em; + padding-left: var(--space-xxl) +} + +.padding-left-xxxl { + padding-left: 8.5em; + padding-left: var(--space-xxxl) +} + +.padding-left-xxxxl { + padding-left: 13.75em; + padding-left: var(--space-xxxxl) +} + +.padding-left-component { + padding-left: 1.25em; + padding-left: var(--component-padding) +} + +.padding-x-md { + padding-left: 1.25em; + padding-left: var(--space-md); + padding-right: 1.25em; + padding-right: var(--space-md) +} + +.padding-x-xxxxs { + padding-left: 0.125em; + padding-left: var(--space-xxxxs); + padding-right: 0.125em; + padding-right: var(--space-xxxxs) +} + +.padding-x-xxxs { + padding-left: 0.25em; + padding-left: var(--space-xxxs); + padding-right: 0.25em; + padding-right: var(--space-xxxs) +} + +.padding-x-xxs { + padding-left: 0.375em; + padding-left: var(--space-xxs); + padding-right: 0.375em; + padding-right: var(--space-xxs) +} + +.padding-x-xs { + padding-left: 0.5em; + padding-left: var(--space-xs); + padding-right: 0.5em; + padding-right: var(--space-xs) +} + +.padding-x-sm { + padding-left: 0.75em; + padding-left: var(--space-sm); + padding-right: 0.75em; + padding-right: var(--space-sm) +} + +.padding-x-lg { + padding-left: 2em; + padding-left: var(--space-lg); + padding-right: 2em; + padding-right: var(--space-lg) +} + +.padding-x-xl { + padding-left: 3.25em; + padding-left: var(--space-xl); + padding-right: 3.25em; + padding-right: var(--space-xl) +} + +.padding-x-xxl { + padding-left: 5.25em; + padding-left: var(--space-xxl); + padding-right: 5.25em; + padding-right: var(--space-xxl) +} + +.padding-x-xxxl { + padding-left: 8.5em; + padding-left: var(--space-xxxl); + padding-right: 8.5em; + padding-right: var(--space-xxxl) +} + +.padding-x-xxxxl { + padding-left: 13.75em; + padding-left: var(--space-xxxxl); + padding-right: 13.75em; + padding-right: var(--space-xxxxl) +} + +.padding-x-component { + padding-left: 1.25em; + padding-left: var(--component-padding); + padding-right: 1.25em; + padding-right: var(--component-padding) +} + +.padding-y-md { + padding-top: 1.25em; + padding-top: var(--space-md); + padding-bottom: 1.25em; + padding-bottom: var(--space-md) +} + +.padding-y-xxxxs { + padding-top: 0.125em; + padding-top: var(--space-xxxxs); + padding-bottom: 0.125em; + padding-bottom: var(--space-xxxxs) +} + +.padding-y-xxxs { + padding-top: 0.25em; + padding-top: var(--space-xxxs); + padding-bottom: 0.25em; + padding-bottom: var(--space-xxxs) +} + +.padding-y-xxs { + padding-top: 0.375em; + padding-top: var(--space-xxs); + padding-bottom: 0.375em; + padding-bottom: var(--space-xxs) +} + +.padding-y-xs { + padding-top: 0.5em; + padding-top: var(--space-xs); + padding-bottom: 0.5em; + padding-bottom: var(--space-xs) +} + +.padding-y-sm { + padding-top: 0.75em; + padding-top: var(--space-sm); + padding-bottom: 0.75em; + padding-bottom: var(--space-sm) +} + +.padding-y-lg { + padding-top: 2em; + padding-top: var(--space-lg); + padding-bottom: 2em; + padding-bottom: var(--space-lg) +} + +.padding-y-xl { + padding-top: 3.25em; + padding-top: var(--space-xl); + padding-bottom: 3.25em; + padding-bottom: var(--space-xl) +} + +.padding-y-xxl { + padding-top: 5.25em; + padding-top: var(--space-xxl); + padding-bottom: 5.25em; + padding-bottom: var(--space-xxl) +} + +.padding-y-xxxl { + padding-top: 8.5em; + padding-top: var(--space-xxxl); + padding-bottom: 8.5em; + padding-bottom: var(--space-xxxl) +} + +.padding-y-xxxxl { + padding-top: 13.75em; + padding-top: var(--space-xxxxl); + padding-bottom: 13.75em; + padding-bottom: var(--space-xxxxl) +} + +.padding-y-component { + padding-top: 1.25em; + padding-top: var(--component-padding); + padding-bottom: 1.25em; + padding-bottom: var(--component-padding) +} + +@media not all and (min-width: 32rem) { + .has-padding\@xs { + padding: 0 !important + } +} + +@media not all and (min-width: 48rem) { + .has-padding\@sm { + padding: 0 !important + } +} + +@media not all and (min-width: 64rem) { + .has-padding\@md { + padding: 0 !important + } +} + +@media not all and (min-width: 80rem) { + .has-padding\@lg { + padding: 0 !important + } +} + +@media not all and (min-width: 90rem) { + .has-padding\@xl { + padding: 0 !important + } +} + +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap +} + +.text-replace { + overflow: hidden; + color: transparent; + text-indent: 100%; + white-space: nowrap +} + +.text-center { + text-align: center +} + +.text-left { + text-align: left +} + +.text-right { + text-align: right +} + +@media (min-width: 32rem) { + .text-center\@xs { + text-align: center + } + .text-left\@xs { + text-align: left + } + .text-right\@xs { + text-align: right + } +} + +@media (min-width: 48rem) { + .text-center\@sm { + text-align: center + } + .text-left\@sm { + text-align: left + } + .text-right\@sm { + text-align: right + } +} + +@media (min-width: 64rem) { + .text-center\@md { + text-align: center + } + .text-left\@md { + text-align: left + } + .text-right\@md { + text-align: right + } +} + +@media (min-width: 80rem) { + .text-center\@lg { + text-align: center + } + .text-left\@lg { + text-align: left + } + .text-right\@lg { + text-align: right + } +} + +@media (min-width: 90rem) { + .text-center\@xl { + text-align: center + } + .text-left\@xl { + text-align: left + } + .text-right\@xl { + text-align: right + } +} + +.color-inherit { + color: inherit +} + +.color-contrast-medium { + color: hsl(240, 1%, 48%); + color: var(--color-contrast-medium, #79797c) +} + +.color-contrast-high { + color: hsl(240, 4%, 20%); + color: var(--color-contrast-high, #313135) +} + +.color-contrast-higher { + color: hsl(240, 8%, 12%); + color: var(--color-contrast-higher, #1c1c21) +} + +.color-primary { + color: hsl(220, 90%, 56%); + color: var(--color-primary, #2a6df4) +} + +.color-accent { + color: hsl(355, 90%, 61%); + color: var(--color-accent, #f54251) +} + +.color-success { + color: hsl(94, 48%, 56%); + color: var(--color-success, #88c559) +} + +.color-warning { + color: hsl(46, 100%, 61%); + color: var(--color-warning, #ffd138) +} + +.color-error { + color: hsl(355, 90%, 61%); + color: var(--color-error, #f54251) +} + +.width-100\% { + width: 100% +} + +.height-100\% { + height: 100% +} + +.media-wrapper { + position: relative; + height: 0; + padding-bottom: 56.25% +} + +.media-wrapper iframe, .media-wrapper video, .media-wrapper img { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100% +} + +.media-wrapper video, .media-wrapper img { + -o-object-fit: cover; + object-fit: cover +} + +.media-wrapper--4\:3 { + padding-bottom: 75% +} + +:root, [data-theme="default"] { + --color-primary-darker: hsl(220, 90%, 36%); + --color-primary-darker-h: 220; + --color-primary-darker-s: 90%; + --color-primary-darker-l: 36%; + --color-primary-dark: hsl(220, 90%, 46%); + --color-primary-dark-h: 220; + --color-primary-dark-s: 90%; + --color-primary-dark-l: 46%; + --color-primary: hsl(220, 90%, 56%); + --color-primary-h: 220; + --color-primary-s: 90%; + --color-primary-l: 56%; + --color-primary-light: hsl(220, 90%, 66%); + --color-primary-light-h: 220; + --color-primary-light-s: 90%; + --color-primary-light-l: 66%; + --color-primary-lighter: hsl(220, 90%, 76%); + --color-primary-lighter-h: 220; + --color-primary-lighter-s: 90%; + --color-primary-lighter-l: 76%; + --color-accent-darker: hsl(355, 90%, 41%); + --color-accent-darker-h: 355; + --color-accent-darker-s: 90%; + --color-accent-darker-l: 41%; + --color-accent-dark: hsl(355, 90%, 51%); + --color-accent-dark-h: 355; + --color-accent-dark-s: 90%; + --color-accent-dark-l: 51%; + --color-accent: hsl(355, 90%, 61%); + --color-accent-h: 355; + --color-accent-s: 90%; + --color-accent-l: 61%; + --color-accent-light: hsl(355, 90%, 71%); + --color-accent-light-h: 355; + --color-accent-light-s: 90%; + --color-accent-light-l: 71%; + --color-accent-lighter: hsl(355, 90%, 81%); + --color-accent-lighter-h: 355; + --color-accent-lighter-s: 90%; + --color-accent-lighter-l: 81%; + --color-black: hsl(240, 8%, 12%); + --color-black-h: 240; + --color-black-s: 8%; + --color-black-l: 12%; + --color-white: hsl(0, 0%, 100%); + --color-white-h: 0; + --color-white-s: 0%; + --color-white-l: 100%; + --color-success-darker: hsl(94, 48%, 36%); + --color-success-darker-h: 94; + --color-success-darker-s: 48%; + --color-success-darker-l: 36%; + --color-success-dark: hsl(94, 48%, 46%); + --color-success-dark-h: 94; + --color-success-dark-s: 48%; + --color-success-dark-l: 46%; + --color-success: hsl(94, 48%, 56%); + --color-success-h: 94; + --color-success-s: 48%; + --color-success-l: 56%; + --color-success-light: hsl(94, 48%, 66%); + --color-success-light-h: 94; + --color-success-light-s: 48%; + --color-success-light-l: 66%; + --color-success-lighter: hsl(94, 48%, 76%); + --color-success-lighter-h: 94; + --color-success-lighter-s: 48%; + --color-success-lighter-l: 76%; + --color-error-darker: hsl(355, 90%, 41%); + --color-error-darker-h: 355; + --color-error-darker-s: 90%; + --color-error-darker-l: 41%; + --color-error-dark: hsl(355, 90%, 51%); + --color-error-dark-h: 355; + --color-error-dark-s: 90%; + --color-error-dark-l: 51%; + --color-error: hsl(355, 90%, 61%); + --color-error-h: 355; + --color-error-s: 90%; + --color-error-l: 61%; + --color-error-light: hsl(355, 90%, 71%); + --color-error-light-h: 355; + --color-error-light-s: 90%; + --color-error-light-l: 71%; + --color-error-lighter: hsl(355, 90%, 81%); + --color-error-lighter-h: 355; + --color-error-lighter-s: 90%; + --color-error-lighter-l: 81%; + --color-warning-darker: hsl(46, 100%, 41%); + --color-warning-darker-h: 46; + --color-warning-darker-s: 100%; + --color-warning-darker-l: 41%; + --color-warning-dark: hsl(46, 100%, 51%); + --color-warning-dark-h: 46; + --color-warning-dark-s: 100%; + --color-warning-dark-l: 51%; + --color-warning: hsl(46, 100%, 61%); + --color-warning-h: 46; + --color-warning-s: 100%; + --color-warning-l: 61%; + --color-warning-light: hsl(46, 100%, 71%); + --color-warning-light-h: 46; + --color-warning-light-s: 100%; + --color-warning-light-l: 71%; + --color-warning-lighter: hsl(46, 100%, 81%); + --color-warning-lighter-h: 46; + --color-warning-lighter-s: 100%; + --color-warning-lighter-l: 81%; + --color-bg: hsl(0, 0%, 100%); + --color-bg-h: 0; + --color-bg-s: 0%; + --color-bg-l: 100%; + --color-contrast-lower: hsl(0, 0%, 95%); + --color-contrast-lower-h: 0; + --color-contrast-lower-s: 0%; + --color-contrast-lower-l: 95%; + --color-contrast-low: hsl(240, 1%, 83%); + --color-contrast-low-h: 240; + --color-contrast-low-s: 1%; + --color-contrast-low-l: 83%; + --color-contrast-medium: hsl(240, 1%, 48%); + --color-contrast-medium-h: 240; + --color-contrast-medium-s: 1%; + --color-contrast-medium-l: 48%; + --color-contrast-high: hsl(240, 4%, 20%); + --color-contrast-high-h: 240; + --color-contrast-high-s: 4%; + --color-contrast-high-l: 20%; + --color-contrast-higher: hsl(240, 8%, 12%); + --color-contrast-higher-h: 240; + --color-contrast-higher-s: 8%; + --color-contrast-higher-l: 12% +} + +@supports (--css: variables) { + @media (min-width: 64rem) { + :root { + --space-unit: 1.25em + } + } +} + +:root { + --radius: 0.25em +} + +:root { + --font-primary: sans-serif; + --text-base-size: 1em; + --text-scale-ratio: 1.2; + --text-xs: calc(1em/var(--text-scale-ratio)/var(--text-scale-ratio)); + --text-sm: calc(var(--text-xs)*var(--text-scale-ratio)); + --text-md: calc(var(--text-sm)*var(--text-scale-ratio)*var(--text-scale-ratio)); + --text-lg: calc(var(--text-md)*var(--text-scale-ratio)); + --text-xl: calc(var(--text-lg)*var(--text-scale-ratio)); + --text-xxl: calc(var(--text-xl)*var(--text-scale-ratio)); + --text-xxxl: calc(var(--text-xxl)*var(--text-scale-ratio)); + --body-line-height: 1.4; + --heading-line-height: 1.2; + --font-primary-capital-letter: 1 +} + +@supports (--css: variables) { + @media (min-width: 64rem) { + :root { + --text-base-size: 1.25em; + --text-scale-ratio: 1.25 + } + } +} + +mark { + background-color: hsla(355, 90%, 61%, 0.2); + background-color: hsla(var(--color-accent-h), var(--color-accent-s), var(--color-accent-l), 0.2); + color: inherit +} + +.text-component { + --line-height-multiplier: 1; + --text-vspace-multiplier: 1 +} + +.text-component blockquote { + padding-left: 1em; + border-left: 4px solid hsl(240, 1%, 83%); + border-left: 4px solid var(--color-contrast-low) +} + +.text-component hr { + background: hsl(240, 1%, 83%); + background: var(--color-contrast-low); + height: 1px +} + +.text-component figcaption { + font-size: 0.83333em; + font-size: var(--text-sm); + color: hsl(240, 1%, 48%); + color: var(--color-contrast-medium) +} + +.article.text-component { + --line-height-multiplier: 1.13; + --text-vspace-multiplier: 1.2 +} + +:root { + --btn-font-size: 1em; + --btn-font-size-sm: calc(var(--btn-font-size) - 0.2em); + --btn-font-size-md: calc(var(--btn-font-size) + 0.2em); + --btn-font-size-lg: calc(var(--btn-font-size) + 0.4em); + --btn-radius: 0.25em; + --btn-padding-x: var(--space-sm); + --btn-padding-y: var(--space-xs) +} + +.btn { + --color-shadow: hsla(240, 8%, 12%, 0.15); + --color-shadow: hsla(var(--color-black-h), var(--color-black-s), var(--color-black-l), 0.15); + box-shadow: 0 4px 16px hsla(240, 8%, 12%, 0.15); + box-shadow: 0 4px 16px hsla(var(--color-black-h), var(--color-black-s), var(--color-black-l), 0.15); + cursor: pointer +} + +.btn--primary { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale +} + +.btn--accent { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale +} + +.btn--disabled { + opacity: 0.6 +} + +:root { + --form-control-padding-x: var(--space-sm); + --form-control-padding-y: var(--space-xs); + --form-control-radius: 0.25em +} + +.form-control { + border: 2px solid hsl(240, 1%, 83%); + border: 2px solid var(--color-contrast-low) +} + +.form-control:focus { + outline: none; + border-color: hsl(220, 90%, 56%); + border-color: var(--color-primary); + --color-shadow: hsla(220, 90%, 56%, 0.2); + --color-shadow: hsla(var(--color-primary-h), var(--color-primary-s), var(--color-primary-l), 0.2); + box-shadow: undefined; + box-shadow: 0 0 0 3px var(--color-shadow) +} + +.form-control:focus:focus { + box-shadow: 0 0 0 3px hsla(220, 90%, 56%, 0.2); + box-shadow: 0 0 0 3px var(--color-shadow) +} + +.form-control[aria-invalid="true"] { + border-color: hsl(355, 90%, 61%); + border-color: var(--color-error) +} + +.form-control[aria-invalid="true"]:focus { + --color-shadow: hsla(355, 90%, 61%, 0.2); + --color-shadow: hsla(var(--color-error-h), var(--color-error-s), var(--color-error-l), 0.2); + box-shadow: undefined; + box-shadow: 0 0 0 3px var(--color-shadow) +} + +.form-control[aria-invalid="true"]:focus:focus { + box-shadow: 0 0 0 3px hsla(355, 90%, 61%, 0.2); + box-shadow: 0 0 0 3px var(--color-shadow) +} + +.form-label { + font-size: 0.83333em; + font-size: var(--text-sm) +} + +:root { + --cd-color-1: hsl(206, 21%, 24%); + --cd-color-1-h: 206; + --cd-color-1-s: 21%; + --cd-color-1-l: 24%; + --cd-color-2: hsl(205, 38%, 89%); + --cd-color-2-h: 205; + --cd-color-2-s: 38%; + --cd-color-2-l: 89%; + --cd-color-3: hsl(207, 10%, 55%); + --cd-color-3-h: 207; + --cd-color-3-s: 10%; + --cd-color-3-l: 55%; + --cd-color-4: hsl(111, 51%, 60%); + --cd-color-4-h: 111; + --cd-color-4-s: 51%; + --cd-color-4-l: 60%; + --cd-color-5: hsl(356, 53%, 49%); + --cd-color-5-h: 356; + --cd-color-5-s: 53%; + --cd-color-5-l: 49%; + --cd-color-6: hsl(47, 85%, 61%); + --cd-color-6-h: 47; + --cd-color-6-s: 85%; + --cd-color-6-l: 61%; + --cd-header-height: 200px; + --font-primary: 'Droid Serif', serif; + --font-secondary: 'Open Sans', sans-serif +} + +@supports (--css: variables) { + @media (min-width: 64rem) { + :root { + --cd-header-height: 300px + } + } +} + +.cd-main-header { + height: 200px; + height: var(--cd-header-height); + background: hsl(206, 21%, 24%); + background: var(--cd-color-1); + color: hsl(0, 0%, 100%); + color: var(--color-white); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale +} + +.cd-main-header h1 { + font-family: 'Open Sans', sans-serif; + font-family: var(--font-secondary); + color: inherit +} + +.cd-timeline { + overflow: hidden; + padding: 2em 0; + padding: var(--space-lg) 0; + color: hsl(207, 10%, 55%); + color: var(--cd-color-3); + background-color: hsl(205, 38%, 93.45%); + background-color: hsl(var(--cd-color-2-h), var(--cd-color-2-s), calc(var(--cd-color-2-l)*1.05)); + font-family: 'Droid Serif', serif; + font-family: var(--font-primary) +} + +.cd-timeline h2 { + font-family: 'Open Sans', sans-serif; + font-family: var(--font-secondary); + font-weight: 700 +} + +.cd-timeline__container { + position: relative; + padding: 1.25em 0; + padding: var(--space-md) 0 +} + +.cd-timeline__container::before { + content: ''; + position: absolute; + top: 0; + left: 18px; + height: 100%; + width: 4px; + background: hsl(205, 38%, 89%); + background: var(--cd-color-2) +} + +@media (min-width: 64rem) { + .cd-timeline__container::before { + left: 50%; + -webkit-transform: translateX(-50%); + -ms-transform: translateX(-50%); + transform: translateX(-50%) + } +} + +.cd-timeline__block { + display: -ms-flexbox; + display: flex; + position: relative; + z-index: 1; + margin-bottom: 2em; + margin-bottom: var(--space-lg) +} + +.cd-timeline__block:last-child { + margin-bottom: 0 +} + +@media (min-width: 64rem) { + .cd-timeline__block:nth-child(even) { + -ms-flex-direction: row-reverse; + flex-direction: row-reverse + } +} + +.cd-timeline__img { + display: -ms-flexbox; + display: flex; + -ms-flex-pack: center; + justify-content: center; + -ms-flex-align: center; + align-items: center; + -ms-flex-negative: 0; + flex-shrink: 0; + width: 40px; + height: 40px; + border-radius: 50%; + box-shadow: 0 0 0 4px hsl(0, 0%, 100%), inset 0 2px 0 rgba(0, 0, 0, 0.08), 0 3px 0 4px rgba(0, 0, 0, 0.05); + box-shadow: 0 0 0 4px var(--color-white), inset 0 2px 0 rgba(0, 0, 0, 0.08), 0 3px 0 4px rgba(0, 0, 0, 0.05) +} + +.cd-timeline__img img { + width: 24px; + height: 24px +} + +@media (min-width: 64rem) { + .cd-timeline__img { + width: 60px; + height: 60px; + -ms-flex-order: 1; + order: 1; + margin-left: calc(5% - 30px); + will-change: transform + } + .cd-timeline__block:nth-child(even) .cd-timeline__img { + margin-right: calc(5% - 30px) + } +} + +.cd-timeline__img--picture { + background-color: hsl(111, 51%, 60%); + background-color: var(--cd-color-4) +} + +.cd-timeline__img--movie { + background-color: hsl(356, 53%, 49%); + background-color: var(--cd-color-5) +} + +.cd-timeline__img--location { + background-color: hsl(47, 85%, 61%); + background-color: var(--cd-color-6) +} + +.cd-timeline__content { + -ms-flex-positive: 1; + flex-grow: 1; + position: relative; + margin-left: 1.25em; + margin-left: var(--space-md); + background: hsl(0, 0%, 100%); + background: var(--color-white); + border-radius: 0.25em; + border-radius: var(--radius-md); + padding: 1.25em; + padding: var(--space-md); + box-shadow: 0 3px 0 hsl(205, 38%, 89%); + box-shadow: 0 3px 0 var(--cd-color-2) +} + +.cd-timeline__content::before { + content: ''; + position: absolute; + top: 16px; + right: 100%; + width: 0; + height: 0; + border: 7px solid transparent; + border-right-color: hsl(0, 0%, 100%); + border-right-color: var(--color-white) +} + +.cd-timeline__content h2 { + color: hsl(206, 21%, 24%); + color: var(--cd-color-1) +} + +@media (min-width: 64rem) { + .cd-timeline__content { + width: 45%; + -ms-flex-positive: 0; + flex-grow: 0; + will-change: transform; + margin: 0; + font-size: 0.8em; + --line-height-multiplier: 1.2 + } + .cd-timeline__content::before { + top: 24px + } + .cd-timeline__block:nth-child(odd) .cd-timeline__content::before { + right: auto; + left: 100%; + width: 0; + height: 0; + border: 7px solid transparent; + border-left-color: hsl(0, 0%, 100%); + border-left-color: var(--color-white) + } +} + +.cd-timeline__date { + color: hsla(207, 10%, 55%, 0.7); + color: hsla(var(--cd-color-3-h), var(--cd-color-3-s), var(--cd-color-3-l), 0.7) +} + +@media (min-width: 64rem) { + .cd-timeline__date { + position: absolute; + width: 100%; + left: 120%; + top: 20px + } + .cd-timeline__block:nth-child(even) .cd-timeline__date { + left: auto; + right: 120%; + text-align: right + } +} + +@media (min-width: 64rem) { + .cd-timeline__img--hidden, .cd-timeline__content--hidden { + visibility: hidden + } + .cd-timeline__img--bounce-in { + -webkit-animation: cd-bounce-1 0.6s; + animation: cd-bounce-1 0.6s + } + .cd-timeline__content--bounce-in { + -webkit-animation: cd-bounce-2 0.6s; + animation: cd-bounce-2 0.6s + } + .cd-timeline__block:nth-child(even) .cd-timeline__content--bounce-in { + -webkit-animation-name: cd-bounce-2-inverse; + animation-name: cd-bounce-2-inverse + } +} + +@-webkit-keyframes cd-bounce-1 { + 0% { + opacity: 0; + -webkit-transform: scale(0.5); + transform: scale(0.5) + } + 60% { + opacity: 1; + -webkit-transform: scale(1.2); + transform: scale(1.2) + } + 100% { + -webkit-transform: scale(1); + transform: scale(1) + } +} + +@keyframes cd-bounce-1 { + 0% { + opacity: 0; + -webkit-transform: scale(0.5); + transform: scale(0.5) + } + 60% { + opacity: 1; + -webkit-transform: scale(1.2); + transform: scale(1.2) + } + 100% { + -webkit-transform: scale(1); + transform: scale(1) + } +} + +@-webkit-keyframes cd-bounce-2 { + 0% { + opacity: 0; + -webkit-transform: translateX(-100px); + transform: translateX(-100px) + } + 60% { + opacity: 1; + -webkit-transform: translateX(20px); + transform: translateX(20px) + } + 100% { + -webkit-transform: translateX(0); + transform: translateX(0) + } +} + +@keyframes cd-bounce-2 { + 0% { + opacity: 0; + -webkit-transform: translateX(-100px); + transform: translateX(-100px) + } + 60% { + opacity: 1; + -webkit-transform: translateX(20px); + transform: translateX(20px) + } + 100% { + -webkit-transform: translateX(0); + transform: translateX(0) + } +} + +@-webkit-keyframes cd-bounce-2-inverse { + 0% { + opacity: 0; + -webkit-transform: translateX(100px); + transform: translateX(100px) + } + 60% { + opacity: 1; + -webkit-transform: translateX(-20px); + transform: translateX(-20px) + } + 100% { + -webkit-transform: translateX(0); + transform: translateX(0) + } +} + +@keyframes cd-bounce-2-inverse { + 0% { + opacity: 0; + -webkit-transform: translateX(100px); + transform: translateX(100px) + } + 60% { + opacity: 1; + -webkit-transform: translateX(-20px); + transform: translateX(-20px) + } + 100% { + -webkit-transform: translateX(0); + transform: translateX(0) + } +} -- cgit v1.2.3 From abb69b911e646766ab1703ddc0534cb82a212e75 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sat, 10 Oct 2020 13:30:25 +0100 Subject: Major fixes to timeline CSS to prevent overriding our CSS --- pydis_site/static/css/home/style.css | 3730 ------------------------------- pydis_site/static/css/home/timeline.css | 3674 ++++++++++++++++++++++++++++++ 2 files changed, 3674 insertions(+), 3730 deletions(-) delete mode 100644 pydis_site/static/css/home/style.css create mode 100644 pydis_site/static/css/home/timeline.css diff --git a/pydis_site/static/css/home/style.css b/pydis_site/static/css/home/style.css deleted file mode 100644 index 7085bf5a..00000000 --- a/pydis_site/static/css/home/style.css +++ /dev/null @@ -1,3730 +0,0 @@ -@import url("https://fonts.googleapis.com/css?family=Droid+Serif|Open+Sans:400,700"); -*, *::after, *::before { - box-sizing: inherit -} - -html { - box-sizing: border-box -} - -body { - background-color: hsl(0, 0%, 100%); - background-color: var(--color-bg, white) -} - -article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section, main, form legend { - display: block -} - -ol, ul { - list-style: none -} - -blockquote, q { - quotes: none -} - -button, input, textarea, select { - margin: 0 -} - -.btn, .form-control, .link, .reset { - background-color: transparent; - padding: 0; - border: 0; - border-radius: 0; - color: inherit; - line-height: inherit; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none -} - -select.form-control::-ms-expand { - display: none -} - -textarea { - resize: vertical; - overflow: auto; - vertical-align: top -} - -input::-ms-clear { - display: none -} - -table { - border-collapse: collapse; - border-spacing: 0 -} - -img, video, svg { - max-width: 100% -} - -[data-theme] { - background-color: hsl(0, 0%, 100%); - background-color: var(--color-bg, #fff); - color: hsl(240, 4%, 20%); - color: var(--color-contrast-high, #313135) -} - -:root { - --space-unit: 1em; - --space-xxxxs: calc(0.125*var(--space-unit)); - --space-xxxs: calc(0.25*var(--space-unit)); - --space-xxs: calc(0.375*var(--space-unit)); - --space-xs: calc(0.5*var(--space-unit)); - --space-sm: calc(0.75*var(--space-unit)); - --space-md: calc(1.25*var(--space-unit)); - --space-lg: calc(2*var(--space-unit)); - --space-xl: calc(3.25*var(--space-unit)); - --space-xxl: calc(5.25*var(--space-unit)); - --space-xxxl: calc(8.5*var(--space-unit)); - --space-xxxxl: calc(13.75*var(--space-unit)); - --component-padding: var(--space-md) -} - -:root { - --max-width-xxs: 32rem; - --max-width-xs: 38rem; - --max-width-sm: 48rem; - --max-width-md: 64rem; - --max-width-lg: 80rem; - --max-width-xl: 90rem; - --max-width-xxl: 120rem -} - -.container { - width: calc(100% - 1.25em); - width: calc(100% - 2*var(--component-padding)); - margin-left: auto; - margin-right: auto -} - -.max-width-xxs { - max-width: 32rem; - max-width: var(--max-width-xxs) -} - -.max-width-xs { - max-width: 38rem; - max-width: var(--max-width-xs) -} - -.max-width-sm { - max-width: 48rem; - max-width: var(--max-width-sm) -} - -.max-width-md { - max-width: 64rem; - max-width: var(--max-width-md) -} - -.max-width-lg { - max-width: 80rem; - max-width: var(--max-width-lg) -} - -.max-width-xl { - max-width: 90rem; - max-width: var(--max-width-xl) -} - -.max-width-xxl { - max-width: 120rem; - max-width: var(--max-width-xxl) -} - -.max-width-adaptive-sm { - max-width: 38rem; - max-width: var(--max-width-xs) -} - -@media (min-width: 64rem) { - .max-width-adaptive-sm { - max-width: 48rem; - max-width: var(--max-width-sm) - } -} - -.max-width-adaptive-md { - max-width: 38rem; - max-width: var(--max-width-xs) -} - -@media (min-width: 64rem) { - .max-width-adaptive-md { - max-width: 64rem; - max-width: var(--max-width-md) - } -} - -.max-width-adaptive, .max-width-adaptive-lg { - max-width: 38rem; - max-width: var(--max-width-xs) -} - -@media (min-width: 64rem) { - .max-width-adaptive, .max-width-adaptive-lg { - max-width: 64rem; - max-width: var(--max-width-md) - } -} - -@media (min-width: 90rem) { - .max-width-adaptive, .max-width-adaptive-lg { - max-width: 80rem; - max-width: var(--max-width-lg) - } -} - -.max-width-adaptive-xl { - max-width: 38rem; - max-width: var(--max-width-xs) -} - -@media (min-width: 64rem) { - .max-width-adaptive-xl { - max-width: 64rem; - max-width: var(--max-width-md) - } -} - -@media (min-width: 90rem) { - .max-width-adaptive-xl { - max-width: 90rem; - max-width: var(--max-width-xl) - } -} - -.grid { - --grid-gap: 0px; - display: -ms-flexbox; - display: flex; - -ms-flex-wrap: wrap; - flex-wrap: wrap -} - -.grid>* { - -ms-flex-preferred-size: 100%; - flex-basis: 100% -} - -[class*="grid-gap"] { - margin-bottom: 1em * -1; - margin-bottom: calc(var(--grid-gap, 1em)*-1); - margin-right: 1em * -1; - margin-right: calc(var(--grid-gap, 1em)*-1) -} - -[class*="grid-gap"]>* { - margin-bottom: 1em; - margin-bottom: var(--grid-gap, 1em); - margin-right: 1em; - margin-right: var(--grid-gap, 1em) -} - -.grid-gap-xxxxs { - --grid-gap: var(--space-xxxxs) -} - -.grid-gap-xxxs { - --grid-gap: var(--space-xxxs) -} - -.grid-gap-xxs { - --grid-gap: var(--space-xxs) -} - -.grid-gap-xs { - --grid-gap: var(--space-xs) -} - -.grid-gap-sm { - --grid-gap: var(--space-sm) -} - -.grid-gap-md { - --grid-gap: var(--space-md) -} - -.grid-gap-lg { - --grid-gap: var(--space-lg) -} - -.grid-gap-xl { - --grid-gap: var(--space-xl) -} - -.grid-gap-xxl { - --grid-gap: var(--space-xxl) -} - -.grid-gap-xxxl { - --grid-gap: var(--space-xxxl) -} - -.grid-gap-xxxxl { - --grid-gap: var(--space-xxxxl) -} - -.col { - -ms-flex-positive: 1; - flex-grow: 1; - -ms-flex-preferred-size: 0; - flex-basis: 0; - max-width: 100% -} - -.col-1 { - -ms-flex-preferred-size: calc(8.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(8.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(8.33% - 0.01px - 1em); - flex-basis: calc(8.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(8.33% - 0.01px - 1em); - max-width: calc(8.33% - 0.01px - var(--grid-gap, 1em)) -} - -.col-2 { - -ms-flex-preferred-size: calc(16.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(16.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(16.66% - 0.01px - 1em); - flex-basis: calc(16.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(16.66% - 0.01px - 1em); - max-width: calc(16.66% - 0.01px - var(--grid-gap, 1em)) -} - -.col-3 { - -ms-flex-preferred-size: calc(25% - 0.01px - 1em); - -ms-flex-preferred-size: calc(25% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(25% - 0.01px - 1em); - flex-basis: calc(25% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(25% - 0.01px - 1em); - max-width: calc(25% - 0.01px - var(--grid-gap, 1em)) -} - -.col-4 { - -ms-flex-preferred-size: calc(33.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(33.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(33.33% - 0.01px - 1em); - flex-basis: calc(33.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(33.33% - 0.01px - 1em); - max-width: calc(33.33% - 0.01px - var(--grid-gap, 1em)) -} - -.col-5 { - -ms-flex-preferred-size: calc(41.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(41.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(41.66% - 0.01px - 1em); - flex-basis: calc(41.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(41.66% - 0.01px - 1em); - max-width: calc(41.66% - 0.01px - var(--grid-gap, 1em)) -} - -.col-6 { - -ms-flex-preferred-size: calc(50% - 0.01px - 1em); - -ms-flex-preferred-size: calc(50% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(50% - 0.01px - 1em); - flex-basis: calc(50% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(50% - 0.01px - 1em); - max-width: calc(50% - 0.01px - var(--grid-gap, 1em)) -} - -.col-7 { - -ms-flex-preferred-size: calc(58.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(58.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(58.33% - 0.01px - 1em); - flex-basis: calc(58.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(58.33% - 0.01px - 1em); - max-width: calc(58.33% - 0.01px - var(--grid-gap, 1em)) -} - -.col-8 { - -ms-flex-preferred-size: calc(66.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(66.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(66.66% - 0.01px - 1em); - flex-basis: calc(66.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(66.66% - 0.01px - 1em); - max-width: calc(66.66% - 0.01px - var(--grid-gap, 1em)) -} - -.col-9 { - -ms-flex-preferred-size: calc(75% - 0.01px - 1em); - -ms-flex-preferred-size: calc(75% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(75% - 0.01px - 1em); - flex-basis: calc(75% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(75% - 0.01px - 1em); - max-width: calc(75% - 0.01px - var(--grid-gap, 1em)) -} - -.col-10 { - -ms-flex-preferred-size: calc(83.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(83.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(83.33% - 0.01px - 1em); - flex-basis: calc(83.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(83.33% - 0.01px - 1em); - max-width: calc(83.33% - 0.01px - var(--grid-gap, 1em)) -} - -.col-11 { - -ms-flex-preferred-size: calc(91.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(91.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(91.66% - 0.01px - 1em); - flex-basis: calc(91.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(91.66% - 0.01px - 1em); - max-width: calc(91.66% - 0.01px - var(--grid-gap, 1em)) -} - -.col-12 { - -ms-flex-preferred-size: calc(100% - 0.01px - 1em); - -ms-flex-preferred-size: calc(100% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(100% - 0.01px - 1em); - flex-basis: calc(100% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(100% - 0.01px - 1em); - max-width: calc(100% - 0.01px - var(--grid-gap, 1em)) -} - -@media (min-width: 32rem) { - .col\@xs { - -ms-flex-positive: 1; - flex-grow: 1; - -ms-flex-preferred-size: 0; - flex-basis: 0; - max-width: 100% - } - .col-1\@xs { - -ms-flex-preferred-size: calc(8.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(8.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(8.33% - 0.01px - 1em); - flex-basis: calc(8.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(8.33% - 0.01px - 1em); - max-width: calc(8.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-2\@xs { - -ms-flex-preferred-size: calc(16.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(16.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(16.66% - 0.01px - 1em); - flex-basis: calc(16.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(16.66% - 0.01px - 1em); - max-width: calc(16.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-3\@xs { - -ms-flex-preferred-size: calc(25% - 0.01px - 1em); - -ms-flex-preferred-size: calc(25% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(25% - 0.01px - 1em); - flex-basis: calc(25% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(25% - 0.01px - 1em); - max-width: calc(25% - 0.01px - var(--grid-gap, 1em)) - } - .col-4\@xs { - -ms-flex-preferred-size: calc(33.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(33.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(33.33% - 0.01px - 1em); - flex-basis: calc(33.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(33.33% - 0.01px - 1em); - max-width: calc(33.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-5\@xs { - -ms-flex-preferred-size: calc(41.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(41.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(41.66% - 0.01px - 1em); - flex-basis: calc(41.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(41.66% - 0.01px - 1em); - max-width: calc(41.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-6\@xs { - -ms-flex-preferred-size: calc(50% - 0.01px - 1em); - -ms-flex-preferred-size: calc(50% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(50% - 0.01px - 1em); - flex-basis: calc(50% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(50% - 0.01px - 1em); - max-width: calc(50% - 0.01px - var(--grid-gap, 1em)) - } - .col-7\@xs { - -ms-flex-preferred-size: calc(58.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(58.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(58.33% - 0.01px - 1em); - flex-basis: calc(58.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(58.33% - 0.01px - 1em); - max-width: calc(58.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-8\@xs { - -ms-flex-preferred-size: calc(66.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(66.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(66.66% - 0.01px - 1em); - flex-basis: calc(66.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(66.66% - 0.01px - 1em); - max-width: calc(66.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-9\@xs { - -ms-flex-preferred-size: calc(75% - 0.01px - 1em); - -ms-flex-preferred-size: calc(75% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(75% - 0.01px - 1em); - flex-basis: calc(75% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(75% - 0.01px - 1em); - max-width: calc(75% - 0.01px - var(--grid-gap, 1em)) - } - .col-10\@xs { - -ms-flex-preferred-size: calc(83.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(83.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(83.33% - 0.01px - 1em); - flex-basis: calc(83.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(83.33% - 0.01px - 1em); - max-width: calc(83.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-11\@xs { - -ms-flex-preferred-size: calc(91.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(91.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(91.66% - 0.01px - 1em); - flex-basis: calc(91.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(91.66% - 0.01px - 1em); - max-width: calc(91.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-12\@xs { - -ms-flex-preferred-size: calc(100% - 0.01px - 1em); - -ms-flex-preferred-size: calc(100% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(100% - 0.01px - 1em); - flex-basis: calc(100% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(100% - 0.01px - 1em); - max-width: calc(100% - 0.01px - var(--grid-gap, 1em)) - } -} - -@media (min-width: 48rem) { - .col\@sm { - -ms-flex-positive: 1; - flex-grow: 1; - -ms-flex-preferred-size: 0; - flex-basis: 0; - max-width: 100% - } - .col-1\@sm { - -ms-flex-preferred-size: calc(8.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(8.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(8.33% - 0.01px - 1em); - flex-basis: calc(8.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(8.33% - 0.01px - 1em); - max-width: calc(8.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-2\@sm { - -ms-flex-preferred-size: calc(16.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(16.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(16.66% - 0.01px - 1em); - flex-basis: calc(16.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(16.66% - 0.01px - 1em); - max-width: calc(16.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-3\@sm { - -ms-flex-preferred-size: calc(25% - 0.01px - 1em); - -ms-flex-preferred-size: calc(25% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(25% - 0.01px - 1em); - flex-basis: calc(25% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(25% - 0.01px - 1em); - max-width: calc(25% - 0.01px - var(--grid-gap, 1em)) - } - .col-4\@sm { - -ms-flex-preferred-size: calc(33.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(33.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(33.33% - 0.01px - 1em); - flex-basis: calc(33.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(33.33% - 0.01px - 1em); - max-width: calc(33.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-5\@sm { - -ms-flex-preferred-size: calc(41.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(41.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(41.66% - 0.01px - 1em); - flex-basis: calc(41.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(41.66% - 0.01px - 1em); - max-width: calc(41.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-6\@sm { - -ms-flex-preferred-size: calc(50% - 0.01px - 1em); - -ms-flex-preferred-size: calc(50% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(50% - 0.01px - 1em); - flex-basis: calc(50% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(50% - 0.01px - 1em); - max-width: calc(50% - 0.01px - var(--grid-gap, 1em)) - } - .col-7\@sm { - -ms-flex-preferred-size: calc(58.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(58.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(58.33% - 0.01px - 1em); - flex-basis: calc(58.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(58.33% - 0.01px - 1em); - max-width: calc(58.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-8\@sm { - -ms-flex-preferred-size: calc(66.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(66.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(66.66% - 0.01px - 1em); - flex-basis: calc(66.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(66.66% - 0.01px - 1em); - max-width: calc(66.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-9\@sm { - -ms-flex-preferred-size: calc(75% - 0.01px - 1em); - -ms-flex-preferred-size: calc(75% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(75% - 0.01px - 1em); - flex-basis: calc(75% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(75% - 0.01px - 1em); - max-width: calc(75% - 0.01px - var(--grid-gap, 1em)) - } - .col-10\@sm { - -ms-flex-preferred-size: calc(83.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(83.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(83.33% - 0.01px - 1em); - flex-basis: calc(83.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(83.33% - 0.01px - 1em); - max-width: calc(83.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-11\@sm { - -ms-flex-preferred-size: calc(91.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(91.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(91.66% - 0.01px - 1em); - flex-basis: calc(91.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(91.66% - 0.01px - 1em); - max-width: calc(91.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-12\@sm { - -ms-flex-preferred-size: calc(100% - 0.01px - 1em); - -ms-flex-preferred-size: calc(100% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(100% - 0.01px - 1em); - flex-basis: calc(100% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(100% - 0.01px - 1em); - max-width: calc(100% - 0.01px - var(--grid-gap, 1em)) - } -} - -@media (min-width: 64rem) { - .col\@md { - -ms-flex-positive: 1; - flex-grow: 1; - -ms-flex-preferred-size: 0; - flex-basis: 0; - max-width: 100% - } - .col-1\@md { - -ms-flex-preferred-size: calc(8.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(8.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(8.33% - 0.01px - 1em); - flex-basis: calc(8.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(8.33% - 0.01px - 1em); - max-width: calc(8.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-2\@md { - -ms-flex-preferred-size: calc(16.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(16.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(16.66% - 0.01px - 1em); - flex-basis: calc(16.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(16.66% - 0.01px - 1em); - max-width: calc(16.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-3\@md { - -ms-flex-preferred-size: calc(25% - 0.01px - 1em); - -ms-flex-preferred-size: calc(25% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(25% - 0.01px - 1em); - flex-basis: calc(25% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(25% - 0.01px - 1em); - max-width: calc(25% - 0.01px - var(--grid-gap, 1em)) - } - .col-4\@md { - -ms-flex-preferred-size: calc(33.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(33.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(33.33% - 0.01px - 1em); - flex-basis: calc(33.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(33.33% - 0.01px - 1em); - max-width: calc(33.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-5\@md { - -ms-flex-preferred-size: calc(41.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(41.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(41.66% - 0.01px - 1em); - flex-basis: calc(41.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(41.66% - 0.01px - 1em); - max-width: calc(41.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-6\@md { - -ms-flex-preferred-size: calc(50% - 0.01px - 1em); - -ms-flex-preferred-size: calc(50% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(50% - 0.01px - 1em); - flex-basis: calc(50% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(50% - 0.01px - 1em); - max-width: calc(50% - 0.01px - var(--grid-gap, 1em)) - } - .col-7\@md { - -ms-flex-preferred-size: calc(58.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(58.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(58.33% - 0.01px - 1em); - flex-basis: calc(58.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(58.33% - 0.01px - 1em); - max-width: calc(58.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-8\@md { - -ms-flex-preferred-size: calc(66.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(66.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(66.66% - 0.01px - 1em); - flex-basis: calc(66.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(66.66% - 0.01px - 1em); - max-width: calc(66.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-9\@md { - -ms-flex-preferred-size: calc(75% - 0.01px - 1em); - -ms-flex-preferred-size: calc(75% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(75% - 0.01px - 1em); - flex-basis: calc(75% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(75% - 0.01px - 1em); - max-width: calc(75% - 0.01px - var(--grid-gap, 1em)) - } - .col-10\@md { - -ms-flex-preferred-size: calc(83.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(83.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(83.33% - 0.01px - 1em); - flex-basis: calc(83.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(83.33% - 0.01px - 1em); - max-width: calc(83.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-11\@md { - -ms-flex-preferred-size: calc(91.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(91.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(91.66% - 0.01px - 1em); - flex-basis: calc(91.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(91.66% - 0.01px - 1em); - max-width: calc(91.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-12\@md { - -ms-flex-preferred-size: calc(100% - 0.01px - 1em); - -ms-flex-preferred-size: calc(100% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(100% - 0.01px - 1em); - flex-basis: calc(100% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(100% - 0.01px - 1em); - max-width: calc(100% - 0.01px - var(--grid-gap, 1em)) - } -} - -@media (min-width: 80rem) { - .col\@lg { - -ms-flex-positive: 1; - flex-grow: 1; - -ms-flex-preferred-size: 0; - flex-basis: 0; - max-width: 100% - } - .col-1\@lg { - -ms-flex-preferred-size: calc(8.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(8.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(8.33% - 0.01px - 1em); - flex-basis: calc(8.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(8.33% - 0.01px - 1em); - max-width: calc(8.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-2\@lg { - -ms-flex-preferred-size: calc(16.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(16.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(16.66% - 0.01px - 1em); - flex-basis: calc(16.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(16.66% - 0.01px - 1em); - max-width: calc(16.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-3\@lg { - -ms-flex-preferred-size: calc(25% - 0.01px - 1em); - -ms-flex-preferred-size: calc(25% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(25% - 0.01px - 1em); - flex-basis: calc(25% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(25% - 0.01px - 1em); - max-width: calc(25% - 0.01px - var(--grid-gap, 1em)) - } - .col-4\@lg { - -ms-flex-preferred-size: calc(33.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(33.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(33.33% - 0.01px - 1em); - flex-basis: calc(33.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(33.33% - 0.01px - 1em); - max-width: calc(33.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-5\@lg { - -ms-flex-preferred-size: calc(41.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(41.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(41.66% - 0.01px - 1em); - flex-basis: calc(41.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(41.66% - 0.01px - 1em); - max-width: calc(41.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-6\@lg { - -ms-flex-preferred-size: calc(50% - 0.01px - 1em); - -ms-flex-preferred-size: calc(50% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(50% - 0.01px - 1em); - flex-basis: calc(50% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(50% - 0.01px - 1em); - max-width: calc(50% - 0.01px - var(--grid-gap, 1em)) - } - .col-7\@lg { - -ms-flex-preferred-size: calc(58.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(58.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(58.33% - 0.01px - 1em); - flex-basis: calc(58.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(58.33% - 0.01px - 1em); - max-width: calc(58.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-8\@lg { - -ms-flex-preferred-size: calc(66.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(66.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(66.66% - 0.01px - 1em); - flex-basis: calc(66.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(66.66% - 0.01px - 1em); - max-width: calc(66.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-9\@lg { - -ms-flex-preferred-size: calc(75% - 0.01px - 1em); - -ms-flex-preferred-size: calc(75% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(75% - 0.01px - 1em); - flex-basis: calc(75% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(75% - 0.01px - 1em); - max-width: calc(75% - 0.01px - var(--grid-gap, 1em)) - } - .col-10\@lg { - -ms-flex-preferred-size: calc(83.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(83.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(83.33% - 0.01px - 1em); - flex-basis: calc(83.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(83.33% - 0.01px - 1em); - max-width: calc(83.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-11\@lg { - -ms-flex-preferred-size: calc(91.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(91.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(91.66% - 0.01px - 1em); - flex-basis: calc(91.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(91.66% - 0.01px - 1em); - max-width: calc(91.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-12\@lg { - -ms-flex-preferred-size: calc(100% - 0.01px - 1em); - -ms-flex-preferred-size: calc(100% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(100% - 0.01px - 1em); - flex-basis: calc(100% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(100% - 0.01px - 1em); - max-width: calc(100% - 0.01px - var(--grid-gap, 1em)) - } -} - -@media (min-width: 90rem) { - .col\@xl { - -ms-flex-positive: 1; - flex-grow: 1; - -ms-flex-preferred-size: 0; - flex-basis: 0; - max-width: 100% - } - .col-1\@xl { - -ms-flex-preferred-size: calc(8.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(8.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(8.33% - 0.01px - 1em); - flex-basis: calc(8.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(8.33% - 0.01px - 1em); - max-width: calc(8.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-2\@xl { - -ms-flex-preferred-size: calc(16.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(16.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(16.66% - 0.01px - 1em); - flex-basis: calc(16.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(16.66% - 0.01px - 1em); - max-width: calc(16.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-3\@xl { - -ms-flex-preferred-size: calc(25% - 0.01px - 1em); - -ms-flex-preferred-size: calc(25% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(25% - 0.01px - 1em); - flex-basis: calc(25% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(25% - 0.01px - 1em); - max-width: calc(25% - 0.01px - var(--grid-gap, 1em)) - } - .col-4\@xl { - -ms-flex-preferred-size: calc(33.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(33.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(33.33% - 0.01px - 1em); - flex-basis: calc(33.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(33.33% - 0.01px - 1em); - max-width: calc(33.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-5\@xl { - -ms-flex-preferred-size: calc(41.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(41.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(41.66% - 0.01px - 1em); - flex-basis: calc(41.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(41.66% - 0.01px - 1em); - max-width: calc(41.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-6\@xl { - -ms-flex-preferred-size: calc(50% - 0.01px - 1em); - -ms-flex-preferred-size: calc(50% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(50% - 0.01px - 1em); - flex-basis: calc(50% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(50% - 0.01px - 1em); - max-width: calc(50% - 0.01px - var(--grid-gap, 1em)) - } - .col-7\@xl { - -ms-flex-preferred-size: calc(58.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(58.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(58.33% - 0.01px - 1em); - flex-basis: calc(58.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(58.33% - 0.01px - 1em); - max-width: calc(58.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-8\@xl { - -ms-flex-preferred-size: calc(66.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(66.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(66.66% - 0.01px - 1em); - flex-basis: calc(66.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(66.66% - 0.01px - 1em); - max-width: calc(66.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-9\@xl { - -ms-flex-preferred-size: calc(75% - 0.01px - 1em); - -ms-flex-preferred-size: calc(75% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(75% - 0.01px - 1em); - flex-basis: calc(75% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(75% - 0.01px - 1em); - max-width: calc(75% - 0.01px - var(--grid-gap, 1em)) - } - .col-10\@xl { - -ms-flex-preferred-size: calc(83.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(83.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(83.33% - 0.01px - 1em); - flex-basis: calc(83.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(83.33% - 0.01px - 1em); - max-width: calc(83.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-11\@xl { - -ms-flex-preferred-size: calc(91.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(91.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(91.66% - 0.01px - 1em); - flex-basis: calc(91.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(91.66% - 0.01px - 1em); - max-width: calc(91.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-12\@xl { - -ms-flex-preferred-size: calc(100% - 0.01px - 1em); - -ms-flex-preferred-size: calc(100% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(100% - 0.01px - 1em); - flex-basis: calc(100% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(100% - 0.01px - 1em); - max-width: calc(100% - 0.01px - var(--grid-gap, 1em)) - } -} - -:root { - --radius-sm: calc(var(--radius, 0.25em)/2); - --radius-md: var(--radius, 0.25em); - --radius-lg: calc(var(--radius, 0.25em)*2); - --shadow-sm: 0 1px 2px rgba(0, 0, 0, .085), 0 1px 8px rgba(0, 0, 0, .1); - --shadow-md: 0 1px 8px rgba(0, 0, 0, .1), 0 8px 24px rgba(0, 0, 0, .15); - --shadow-lg: 0 1px 8px rgba(0, 0, 0, .1), 0 16px 48px rgba(0, 0, 0, .1), 0 24px 60px rgba(0, 0, 0, .1); - --bounce: cubic-bezier(0.175, 0.885, 0.32, 1.275); - --ease-in-out: cubic-bezier(0.645, 0.045, 0.355, 1); - --ease-in: cubic-bezier(0.55, 0.055, 0.675, 0.19); - --ease-out: cubic-bezier(0.215, 0.61, 0.355, 1) -} - -:root { - --body-line-height: 1.4; - --heading-line-height: 1.2 -} - -body { - font-size: 1em; - font-size: var(--text-base-size, 1em); - font-family: 'Droid Serif', serif; - font-family: var(--font-primary, sans-serif); - color: hsl(240, 4%, 20%); - color: var(--color-contrast-high, #313135) -} - -h1, h2, h3, h4 { - color: hsl(240, 8%, 12%); - color: var(--color-contrast-higher, #1c1c21); - line-height: 1.2; - line-height: var(--heading-line-height, 1.2) -} - -.text-xxxl { - font-size: 2.48832em; - font-size: var(--text-xxxl, 2.488em) -} - -h1, .text-xxl { - font-size: 2.0736em; - font-size: var(--text-xxl, 2.074em) -} - -h2, .text-xl { - font-size: 1.728em; - font-size: var(--text-xl, 1.728em) -} - -h3, .text-lg { - font-size: 1.44em; - font-size: var(--text-lg, 1.44em) -} - -h4, .text-md { - font-size: 1.2em; - font-size: var(--text-md, 1.2em) -} - -small, .text-sm { - font-size: 0.83333em; - font-size: var(--text-sm, 0.833em) -} - -.text-xs { - font-size: 0.69444em; - font-size: var(--text-xs, 0.694em) -} - -a, .link { - color: hsl(220, 90%, 56%); - color: var(--color-primary, #2a6df4); - text-decoration: underline -} - -strong, .text-bold { - font-weight: bold -} - -s { - text-decoration: line-through -} - -u, .text-underline { - text-decoration: underline -} - -.text-component { - --component-body-line-height: calc(var(--body-line-height)*var(--line-height-multiplier, 1)); - --component-heading-line-height: calc(var(--heading-line-height)*var(--line-height-multiplier, 1)) -} - -.text-component h1, .text-component h2, .text-component h3, .text-component h4 { - line-height: 1.2; - line-height: var(--component-heading-line-height, 1.2); - margin-bottom: 0.25em; - margin-bottom: calc(var(--space-xxxs)*var(--text-vspace-multiplier, 1)) -} - -.text-component h2, .text-component h3, .text-component h4 { - margin-top: 0.75em; - margin-top: calc(var(--space-sm)*var(--text-vspace-multiplier, 1)) -} - -.text-component p, .text-component blockquote, .text-component ul li, .text-component ol li { - line-height: 1.4; - line-height: var(--component-body-line-height) -} - -.text-component ul, .text-component ol, .text-component p, .text-component blockquote, .text-component .text-component__block { - margin-bottom: 0.75em; - margin-bottom: calc(var(--space-sm)*var(--text-vspace-multiplier, 1)) -} - -.text-component ul, .text-component ol { - padding-left: 1em -} - -.text-component ul { - list-style-type: disc -} - -.text-component ol { - list-style-type: decimal -} - -.text-component img { - display: block; - margin: 0 auto -} - -.text-component figcaption { - text-align: center; - margin-top: 0.5em; - margin-top: var(--space-xs) -} - -.text-component em { - font-style: italic -} - -.text-component hr { - margin-top: 2em; - margin-top: calc(var(--space-lg)*var(--text-vspace-multiplier, 1)); - margin-bottom: 2em; - margin-bottom: calc(var(--space-lg)*var(--text-vspace-multiplier, 1)); - margin-left: auto; - margin-right: auto -} - -.text-component>*:first-child { - margin-top: 0 -} - -.text-component>*:last-child { - margin-bottom: 0 -} - -.text-component__block--full-width { - width: 100vw; - margin-left: calc(50% - 50vw) -} - -@media (min-width: 48rem) { - .text-component__block--left, .text-component__block--right { - width: 45% - } - .text-component__block--left img, .text-component__block--right img { - width: 100% - } - .text-component__block--left { - float: left; - margin-right: 0.75em; - margin-right: calc(var(--space-sm)*var(--text-vspace-multiplier, 1)) - } - .text-component__block--right { - float: right; - margin-left: 0.75em; - margin-left: calc(var(--space-sm)*var(--text-vspace-multiplier, 1)) - } -} - -@media (min-width: 90rem) { - .text-component__block--outset { - width: calc(100% + 10.5em); - width: calc(100% + 2*var(--space-xxl)) - } - .text-component__block--outset img { - width: 100% - } - .text-component__block--outset:not(.text-component__block--right) { - margin-left: -5.25em; - margin-left: calc(-1*var(--space-xxl)) - } - .text-component__block--left, .text-component__block--right { - width: 50% - } - .text-component__block--right.text-component__block--outset { - margin-right: -5.25em; - margin-right: calc(-1*var(--space-xxl)) - } -} - -:root { - --icon-xxs: 12px; - --icon-xs: 16px; - --icon-sm: 24px; - --icon-md: 32px; - --icon-lg: 48px; - --icon-xl: 64px; - --icon-xxl: 128px -} - -.icon { - display: inline-block; - color: inherit; - fill: currentColor; - height: 1em; - width: 1em; - line-height: 1; - -ms-flex-negative: 0; - flex-shrink: 0 -} - -.icon--xxs { - font-size: 12px; - font-size: var(--icon-xxs) -} - -.icon--xs { - font-size: 16px; - font-size: var(--icon-xs) -} - -.icon--sm { - font-size: 24px; - font-size: var(--icon-sm) -} - -.icon--md { - font-size: 32px; - font-size: var(--icon-md) -} - -.icon--lg { - font-size: 48px; - font-size: var(--icon-lg) -} - -.icon--xl { - font-size: 64px; - font-size: var(--icon-xl) -} - -.icon--xxl { - font-size: 128px; - font-size: var(--icon-xxl) -} - -.icon--is-spinning { - -webkit-animation: icon-spin 1s infinite linear; - animation: icon-spin 1s infinite linear -} - -@-webkit-keyframes icon-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg) - } - 100% { - -webkit-transform: rotate(360deg); - transform: rotate(360deg) - } -} - -@keyframes icon-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg) - } - 100% { - -webkit-transform: rotate(360deg); - transform: rotate(360deg) - } -} - -.icon use { - color: inherit; - fill: currentColor -} - -.btn { - position: relative; - display: -ms-inline-flexbox; - display: inline-flex; - -ms-flex-pack: center; - justify-content: center; - -ms-flex-align: center; - align-items: center; - white-space: nowrap; - text-decoration: none; - line-height: 1; - font-size: 1em; - font-size: var(--btn-font-size, 1em); - padding-top: 0.5em; - padding-top: var(--btn-padding-y, 0.5em); - padding-bottom: 0.5em; - padding-bottom: var(--btn-padding-y, 0.5em); - padding-left: 0.75em; - padding-left: var(--btn-padding-x, 0.75em); - padding-right: 0.75em; - padding-right: var(--btn-padding-x, 0.75em); - border-radius: 0.25em; - border-radius: var(--btn-radius, 0.25em) -} - -.btn--primary { - background-color: hsl(220, 90%, 56%); - background-color: var(--color-primary, #2a6df4); - color: hsl(0, 0%, 100%); - color: var(--color-white, #fff) -} - -.btn--subtle { - background-color: hsl(240, 1%, 83%); - background-color: var(--color-contrast-low, #d3d3d4); - color: hsl(240, 8%, 12%); - color: var(--color-contrast-higher, #1c1c21) -} - -.btn--accent { - background-color: hsl(355, 90%, 61%); - background-color: var(--color-accent, #f54251); - color: hsl(0, 0%, 100%); - color: var(--color-white, #fff) -} - -.btn--disabled { - cursor: not-allowed -} - -.btn--sm { - font-size: 0.8em; - font-size: var(--btn-font-size-sm, 0.8em) -} - -.btn--md { - font-size: 1.2em; - font-size: var(--btn-font-size-md, 1.2em) -} - -.btn--lg { - font-size: 1.4em; - font-size: var(--btn-font-size-lg, 1.4em) -} - -.btn--icon { - padding: 0.5em; - padding: var(--btn-padding-y, 0.5em) -} - -.form-control { - background-color: hsl(0, 0%, 100%); - background-color: var(--color-bg, #f2f2f2); - padding-top: 0.5em; - padding-top: var(--form-control-padding-y, 0.5em); - padding-bottom: 0.5em; - padding-bottom: var(--form-control-padding-y, 0.5em); - padding-left: 0.75em; - padding-left: var(--form-control-padding-x, 0.75em); - padding-right: 0.75em; - padding-right: var(--form-control-padding-x, 0.75em); - border-radius: 0.25em; - border-radius: var(--form-control-radius, 0.25em) -} - -.form-control::-webkit-input-placeholder { - color: hsl(240, 1%, 48%); - color: var(--color-contrast-medium, #79797c) -} - -.form-control::-moz-placeholder { - opacity: 1; - color: hsl(240, 1%, 48%); - color: var(--color-contrast-medium, #79797c) -} - -.form-control:-ms-input-placeholder { - color: hsl(240, 1%, 48%); - color: var(--color-contrast-medium, #79797c) -} - -.form-control:-moz-placeholder { - color: hsl(240, 1%, 48%); - color: var(--color-contrast-medium, #79797c) -} - -.form-control[disabled], .form-control[readonly] { - cursor: not-allowed -} - -.form-legend { - color: hsl(240, 8%, 12%); - color: var(--color-contrast-higher, #1c1c21); - line-height: 1.2; - font-size: 1.2em; - font-size: var(--text-md, 1.2em); - margin-bottom: 0.375em; - margin-bottom: var(--space-xxs) -} - -.form-label { - display: inline-block -} - -.form__msg-error { - background-color: hsl(355, 90%, 61%); - background-color: var(--color-error, #f54251); - color: hsl(0, 0%, 100%); - color: var(--color-white, #fff); - font-size: 0.83333em; - font-size: var(--text-sm, 0.833em); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - padding: 0.5em; - padding: var(--space-xs); - margin-top: 0.75em; - margin-top: var(--space-sm); - border-radius: 0.25em; - border-radius: var(--radius-md, 0.25em); - position: absolute; - clip: rect(1px, 1px, 1px, 1px) -} - -.form__msg-error::before { - content: ''; - position: absolute; - left: 0.75em; - left: var(--space-sm); - top: 0; - -webkit-transform: translateY(-100%); - -ms-transform: translateY(-100%); - transform: translateY(-100%); - width: 0; - height: 0; - border: 8px solid transparent; - border-bottom-color: hsl(355, 90%, 61%); - border-bottom-color: var(--color-error) -} - -.form__msg-error--is-visible { - position: relative; - clip: auto -} - -.radio-list>*, .checkbox-list>* { - position: relative; - display: -ms-flexbox; - display: flex; - -ms-flex-align: baseline; - align-items: baseline; - margin-bottom: 0.375em; - margin-bottom: var(--space-xxs) -} - -.radio-list>*:last-of-type, .checkbox-list>*:last-of-type { - margin-bottom: 0 -} - -.radio-list label, .checkbox-list label { - line-height: 1.4; - line-height: var(--body-line-height); - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none -} - -.radio-list input, .checkbox-list input { - vertical-align: top; - margin-right: 0.25em; - margin-right: var(--space-xxxs); - -ms-flex-negative: 0; - flex-shrink: 0 -} - -:root { - --zindex-header: 2; - --zindex-popover: 5; - --zindex-fixed-element: 10; - --zindex-overlay: 15 -} - -@media not all and (min-width: 32rem) { - .display\@xs { - display: none !important - } -} - -@media (min-width: 32rem) { - .hide\@xs { - display: none !important - } -} - -@media not all and (min-width: 48rem) { - .display\@sm { - display: none !important - } -} - -@media (min-width: 48rem) { - .hide\@sm { - display: none !important - } -} - -@media not all and (min-width: 64rem) { - .display\@md { - display: none !important - } -} - -@media (min-width: 64rem) { - .hide\@md { - display: none !important - } -} - -@media not all and (min-width: 80rem) { - .display\@lg { - display: none !important - } -} - -@media (min-width: 80rem) { - .hide\@lg { - display: none !important - } -} - -@media not all and (min-width: 90rem) { - .display\@xl { - display: none !important - } -} - -@media (min-width: 90rem) { - .hide\@xl { - display: none !important - } -} - -:root { - --display: block -} - -.is-visible { - display: block !important; - display: var(--display) !important -} - -.is-hidden { - display: none !important -} - -.sr-only { - position: absolute; - clip: rect(1px, 1px, 1px, 1px); - -webkit-clip-path: inset(50%); - clip-path: inset(50%); - width: 1px; - height: 1px; - overflow: hidden; - padding: 0; - border: 0; - white-space: nowrap -} - -.flex { - display: -ms-flexbox; - display: flex -} - -.inline-flex { - display: -ms-inline-flexbox; - display: inline-flex -} - -.flex-wrap { - -ms-flex-wrap: wrap; - flex-wrap: wrap -} - -.flex-column { - -ms-flex-direction: column; - flex-direction: column -} - -.flex-row { - -ms-flex-direction: row; - flex-direction: row -} - -.flex-center { - -ms-flex-pack: center; - justify-content: center; - -ms-flex-align: center; - align-items: center -} - -.justify-start { - -ms-flex-pack: start; - justify-content: flex-start -} - -.justify-end { - -ms-flex-pack: end; - justify-content: flex-end -} - -.justify-center { - -ms-flex-pack: center; - justify-content: center -} - -.justify-between { - -ms-flex-pack: justify; - justify-content: space-between -} - -.items-center { - -ms-flex-align: center; - align-items: center -} - -.items-start { - -ms-flex-align: start; - align-items: flex-start -} - -.items-end { - -ms-flex-align: end; - align-items: flex-end -} - -@media (min-width: 32rem) { - .flex-wrap\@xs { - -ms-flex-wrap: wrap; - flex-wrap: wrap - } - .flex-column\@xs { - -ms-flex-direction: column; - flex-direction: column - } - .flex-row\@xs { - -ms-flex-direction: row; - flex-direction: row - } - .flex-center\@xs { - -ms-flex-pack: center; - justify-content: center; - -ms-flex-align: center; - align-items: center - } - .justify-start\@xs { - -ms-flex-pack: start; - justify-content: flex-start - } - .justify-end\@xs { - -ms-flex-pack: end; - justify-content: flex-end - } - .justify-center\@xs { - -ms-flex-pack: center; - justify-content: center - } - .justify-between\@xs { - -ms-flex-pack: justify; - justify-content: space-between - } - .items-center\@xs { - -ms-flex-align: center; - align-items: center - } - .items-start\@xs { - -ms-flex-align: start; - align-items: flex-start - } - .items-end\@xs { - -ms-flex-align: end; - align-items: flex-end - } -} - -@media (min-width: 48rem) { - .flex-wrap\@sm { - -ms-flex-wrap: wrap; - flex-wrap: wrap - } - .flex-column\@sm { - -ms-flex-direction: column; - flex-direction: column - } - .flex-row\@sm { - -ms-flex-direction: row; - flex-direction: row - } - .flex-center\@sm { - -ms-flex-pack: center; - justify-content: center; - -ms-flex-align: center; - align-items: center - } - .justify-start\@sm { - -ms-flex-pack: start; - justify-content: flex-start - } - .justify-end\@sm { - -ms-flex-pack: end; - justify-content: flex-end - } - .justify-center\@sm { - -ms-flex-pack: center; - justify-content: center - } - .justify-between\@sm { - -ms-flex-pack: justify; - justify-content: space-between - } - .items-center\@sm { - -ms-flex-align: center; - align-items: center - } - .items-start\@sm { - -ms-flex-align: start; - align-items: flex-start - } - .items-end\@sm { - -ms-flex-align: end; - align-items: flex-end - } -} - -@media (min-width: 64rem) { - .flex-wrap\@md { - -ms-flex-wrap: wrap; - flex-wrap: wrap - } - .flex-column\@md { - -ms-flex-direction: column; - flex-direction: column - } - .flex-row\@md { - -ms-flex-direction: row; - flex-direction: row - } - .flex-center\@md { - -ms-flex-pack: center; - justify-content: center; - -ms-flex-align: center; - align-items: center - } - .justify-start\@md { - -ms-flex-pack: start; - justify-content: flex-start - } - .justify-end\@md { - -ms-flex-pack: end; - justify-content: flex-end - } - .justify-center\@md { - -ms-flex-pack: center; - justify-content: center - } - .justify-between\@md { - -ms-flex-pack: justify; - justify-content: space-between - } - .items-center\@md { - -ms-flex-align: center; - align-items: center - } - .items-start\@md { - -ms-flex-align: start; - align-items: flex-start - } - .items-end\@md { - -ms-flex-align: end; - align-items: flex-end - } -} - -@media (min-width: 80rem) { - .flex-wrap\@lg { - -ms-flex-wrap: wrap; - flex-wrap: wrap - } - .flex-column\@lg { - -ms-flex-direction: column; - flex-direction: column - } - .flex-row\@lg { - -ms-flex-direction: row; - flex-direction: row - } - .flex-center\@lg { - -ms-flex-pack: center; - justify-content: center; - -ms-flex-align: center; - align-items: center - } - .justify-start\@lg { - -ms-flex-pack: start; - justify-content: flex-start - } - .justify-end\@lg { - -ms-flex-pack: end; - justify-content: flex-end - } - .justify-center\@lg { - -ms-flex-pack: center; - justify-content: center - } - .justify-between\@lg { - -ms-flex-pack: justify; - justify-content: space-between - } - .items-center\@lg { - -ms-flex-align: center; - align-items: center - } - .items-start\@lg { - -ms-flex-align: start; - align-items: flex-start - } - .items-end\@lg { - -ms-flex-align: end; - align-items: flex-end - } -} - -@media (min-width: 90rem) { - .flex-wrap\@xl { - -ms-flex-wrap: wrap; - flex-wrap: wrap - } - .flex-column\@xl { - -ms-flex-direction: column; - flex-direction: column - } - .flex-row\@xl { - -ms-flex-direction: row; - flex-direction: row - } - .flex-center\@xl { - -ms-flex-pack: center; - justify-content: center; - -ms-flex-align: center; - align-items: center - } - .justify-start\@xl { - -ms-flex-pack: start; - justify-content: flex-start - } - .justify-end\@xl { - -ms-flex-pack: end; - justify-content: flex-end - } - .justify-center\@xl { - -ms-flex-pack: center; - justify-content: center - } - .justify-between\@xl { - -ms-flex-pack: justify; - justify-content: space-between - } - .items-center\@xl { - -ms-flex-align: center; - align-items: center - } - .items-start\@xl { - -ms-flex-align: start; - align-items: flex-start - } - .items-end\@xl { - -ms-flex-align: end; - align-items: flex-end - } -} - -.flex-grow { - -ms-flex-positive: 1; - flex-grow: 1 -} - -.flex-shrink-0 { - -ms-flex-negative: 0; - flex-shrink: 0 -} - -.flex-gap-xxxs { - margin-bottom: -0.25em; - margin-bottom: calc(-1*var(--space-xxxs)); - margin-right: -0.25em; - margin-right: calc(-1*var(--space-xxxs)) -} - -.flex-gap-xxxs>* { - margin-bottom: 0.25em; - margin-bottom: var(--space-xxxs); - margin-right: 0.25em; - margin-right: var(--space-xxxs) -} - -.flex-gap-xxs { - margin-bottom: -0.375em; - margin-bottom: calc(-1*var(--space-xxs)); - margin-right: -0.375em; - margin-right: calc(-1*var(--space-xxs)) -} - -.flex-gap-xxs>* { - margin-bottom: 0.375em; - margin-bottom: var(--space-xxs); - margin-right: 0.375em; - margin-right: var(--space-xxs) -} - -.flex-gap-xs { - margin-bottom: -0.5em; - margin-bottom: calc(-1*var(--space-xs)); - margin-right: -0.5em; - margin-right: calc(-1*var(--space-xs)) -} - -.flex-gap-xs>* { - margin-bottom: 0.5em; - margin-bottom: var(--space-xs); - margin-right: 0.5em; - margin-right: var(--space-xs) -} - -.flex-gap-sm { - margin-bottom: -0.75em; - margin-bottom: calc(-1*var(--space-sm)); - margin-right: -0.75em; - margin-right: calc(-1*var(--space-sm)) -} - -.flex-gap-sm>* { - margin-bottom: 0.75em; - margin-bottom: var(--space-sm); - margin-right: 0.75em; - margin-right: var(--space-sm) -} - -.flex-gap-md { - margin-bottom: -1.25em; - margin-bottom: calc(-1*var(--space-md)); - margin-right: -1.25em; - margin-right: calc(-1*var(--space-md)) -} - -.flex-gap-md>* { - margin-bottom: 1.25em; - margin-bottom: var(--space-md); - margin-right: 1.25em; - margin-right: var(--space-md) -} - -.flex-gap-lg { - margin-bottom: -2em; - margin-bottom: calc(-1*var(--space-lg)); - margin-right: -2em; - margin-right: calc(-1*var(--space-lg)) -} - -.flex-gap-lg>* { - margin-bottom: 2em; - margin-bottom: var(--space-lg); - margin-right: 2em; - margin-right: var(--space-lg) -} - -.flex-gap-xl { - margin-bottom: -3.25em; - margin-bottom: calc(-1*var(--space-xl)); - margin-right: -3.25em; - margin-right: calc(-1*var(--space-xl)) -} - -.flex-gap-xl>* { - margin-bottom: 3.25em; - margin-bottom: var(--space-xl); - margin-right: 3.25em; - margin-right: var(--space-xl) -} - -.flex-gap-xxl { - margin-bottom: -5.25em; - margin-bottom: calc(-1*var(--space-xxl)); - margin-right: -5.25em; - margin-right: calc(-1*var(--space-xxl)) -} - -.flex-gap-xxl>* { - margin-bottom: 5.25em; - margin-bottom: var(--space-xxl); - margin-right: 5.25em; - margin-right: var(--space-xxl) -} - -.margin-xxxxs { - margin: 0.125em; - margin: var(--space-xxxxs) -} - -.margin-xxxs { - margin: 0.25em; - margin: var(--space-xxxs) -} - -.margin-xxs { - margin: 0.375em; - margin: var(--space-xxs) -} - -.margin-xs { - margin: 0.5em; - margin: var(--space-xs) -} - -.margin-sm { - margin: 0.75em; - margin: var(--space-sm) -} - -.margin-md { - margin: 1.25em; - margin: var(--space-md) -} - -.margin-lg { - margin: 2em; - margin: var(--space-lg) -} - -.margin-xl { - margin: 3.25em; - margin: var(--space-xl) -} - -.margin-xxl { - margin: 5.25em; - margin: var(--space-xxl) -} - -.margin-xxxl { - margin: 8.5em; - margin: var(--space-xxxl) -} - -.margin-xxxxl { - margin: 13.75em; - margin: var(--space-xxxxl) -} - -.margin-auto { - margin: auto -} - -.margin-top-xxxxs { - margin-top: 0.125em; - margin-top: var(--space-xxxxs) -} - -.margin-top-xxxs { - margin-top: 0.25em; - margin-top: var(--space-xxxs) -} - -.margin-top-xxs { - margin-top: 0.375em; - margin-top: var(--space-xxs) -} - -.margin-top-xs { - margin-top: 0.5em; - margin-top: var(--space-xs) -} - -.margin-top-sm { - margin-top: 0.75em; - margin-top: var(--space-sm) -} - -.margin-top-md { - margin-top: 1.25em; - margin-top: var(--space-md) -} - -.margin-top-lg { - margin-top: 2em; - margin-top: var(--space-lg) -} - -.margin-top-xl { - margin-top: 3.25em; - margin-top: var(--space-xl) -} - -.margin-top-xxl { - margin-top: 5.25em; - margin-top: var(--space-xxl) -} - -.margin-top-xxxl { - margin-top: 8.5em; - margin-top: var(--space-xxxl) -} - -.margin-top-xxxxl { - margin-top: 13.75em; - margin-top: var(--space-xxxxl) -} - -.margin-top-auto { - margin-top: auto -} - -.margin-bottom-xxxxs { - margin-bottom: 0.125em; - margin-bottom: var(--space-xxxxs) -} - -.margin-bottom-xxxs { - margin-bottom: 0.25em; - margin-bottom: var(--space-xxxs) -} - -.margin-bottom-xxs { - margin-bottom: 0.375em; - margin-bottom: var(--space-xxs) -} - -.margin-bottom-xs { - margin-bottom: 0.5em; - margin-bottom: var(--space-xs) -} - -.margin-bottom-sm { - margin-bottom: 0.75em; - margin-bottom: var(--space-sm) -} - -.margin-bottom-md { - margin-bottom: 1.25em; - margin-bottom: var(--space-md) -} - -.margin-bottom-lg { - margin-bottom: 2em; - margin-bottom: var(--space-lg) -} - -.margin-bottom-xl { - margin-bottom: 3.25em; - margin-bottom: var(--space-xl) -} - -.margin-bottom-xxl { - margin-bottom: 5.25em; - margin-bottom: var(--space-xxl) -} - -.margin-bottom-xxxl { - margin-bottom: 8.5em; - margin-bottom: var(--space-xxxl) -} - -.margin-bottom-xxxxl { - margin-bottom: 13.75em; - margin-bottom: var(--space-xxxxl) -} - -.margin-bottom-auto { - margin-bottom: auto -} - -.margin-right-xxxxs { - margin-right: 0.125em; - margin-right: var(--space-xxxxs) -} - -.margin-right-xxxs { - margin-right: 0.25em; - margin-right: var(--space-xxxs) -} - -.margin-right-xxs { - margin-right: 0.375em; - margin-right: var(--space-xxs) -} - -.margin-right-xs { - margin-right: 0.5em; - margin-right: var(--space-xs) -} - -.margin-right-sm { - margin-right: 0.75em; - margin-right: var(--space-sm) -} - -.margin-right-md { - margin-right: 1.25em; - margin-right: var(--space-md) -} - -.margin-right-lg { - margin-right: 2em; - margin-right: var(--space-lg) -} - -.margin-right-xl { - margin-right: 3.25em; - margin-right: var(--space-xl) -} - -.margin-right-xxl { - margin-right: 5.25em; - margin-right: var(--space-xxl) -} - -.margin-right-xxxl { - margin-right: 8.5em; - margin-right: var(--space-xxxl) -} - -.margin-right-xxxxl { - margin-right: 13.75em; - margin-right: var(--space-xxxxl) -} - -.margin-right-auto { - margin-right: auto -} - -.margin-left-xxxxs { - margin-left: 0.125em; - margin-left: var(--space-xxxxs) -} - -.margin-left-xxxs { - margin-left: 0.25em; - margin-left: var(--space-xxxs) -} - -.margin-left-xxs { - margin-left: 0.375em; - margin-left: var(--space-xxs) -} - -.margin-left-xs { - margin-left: 0.5em; - margin-left: var(--space-xs) -} - -.margin-left-sm { - margin-left: 0.75em; - margin-left: var(--space-sm) -} - -.margin-left-md { - margin-left: 1.25em; - margin-left: var(--space-md) -} - -.margin-left-lg { - margin-left: 2em; - margin-left: var(--space-lg) -} - -.margin-left-xl { - margin-left: 3.25em; - margin-left: var(--space-xl) -} - -.margin-left-xxl { - margin-left: 5.25em; - margin-left: var(--space-xxl) -} - -.margin-left-xxxl { - margin-left: 8.5em; - margin-left: var(--space-xxxl) -} - -.margin-left-xxxxl { - margin-left: 13.75em; - margin-left: var(--space-xxxxl) -} - -.margin-left-auto { - margin-left: auto -} - -.margin-x-xxxxs { - margin-left: 0.125em; - margin-left: var(--space-xxxxs); - margin-right: 0.125em; - margin-right: var(--space-xxxxs) -} - -.margin-x-xxxs { - margin-left: 0.25em; - margin-left: var(--space-xxxs); - margin-right: 0.25em; - margin-right: var(--space-xxxs) -} - -.margin-x-xxs { - margin-left: 0.375em; - margin-left: var(--space-xxs); - margin-right: 0.375em; - margin-right: var(--space-xxs) -} - -.margin-x-xs { - margin-left: 0.5em; - margin-left: var(--space-xs); - margin-right: 0.5em; - margin-right: var(--space-xs) -} - -.margin-x-sm { - margin-left: 0.75em; - margin-left: var(--space-sm); - margin-right: 0.75em; - margin-right: var(--space-sm) -} - -.margin-x-md { - margin-left: 1.25em; - margin-left: var(--space-md); - margin-right: 1.25em; - margin-right: var(--space-md) -} - -.margin-x-lg { - margin-left: 2em; - margin-left: var(--space-lg); - margin-right: 2em; - margin-right: var(--space-lg) -} - -.margin-x-xl { - margin-left: 3.25em; - margin-left: var(--space-xl); - margin-right: 3.25em; - margin-right: var(--space-xl) -} - -.margin-x-xxl { - margin-left: 5.25em; - margin-left: var(--space-xxl); - margin-right: 5.25em; - margin-right: var(--space-xxl) -} - -.margin-x-xxxl { - margin-left: 8.5em; - margin-left: var(--space-xxxl); - margin-right: 8.5em; - margin-right: var(--space-xxxl) -} - -.margin-x-xxxxl { - margin-left: 13.75em; - margin-left: var(--space-xxxxl); - margin-right: 13.75em; - margin-right: var(--space-xxxxl) -} - -.margin-x-auto { - margin-left: auto; - margin-right: auto -} - -.margin-y-xxxxs { - margin-top: 0.125em; - margin-top: var(--space-xxxxs); - margin-bottom: 0.125em; - margin-bottom: var(--space-xxxxs) -} - -.margin-y-xxxs { - margin-top: 0.25em; - margin-top: var(--space-xxxs); - margin-bottom: 0.25em; - margin-bottom: var(--space-xxxs) -} - -.margin-y-xxs { - margin-top: 0.375em; - margin-top: var(--space-xxs); - margin-bottom: 0.375em; - margin-bottom: var(--space-xxs) -} - -.margin-y-xs { - margin-top: 0.5em; - margin-top: var(--space-xs); - margin-bottom: 0.5em; - margin-bottom: var(--space-xs) -} - -.margin-y-sm { - margin-top: 0.75em; - margin-top: var(--space-sm); - margin-bottom: 0.75em; - margin-bottom: var(--space-sm) -} - -.margin-y-md { - margin-top: 1.25em; - margin-top: var(--space-md); - margin-bottom: 1.25em; - margin-bottom: var(--space-md) -} - -.margin-y-lg { - margin-top: 2em; - margin-top: var(--space-lg); - margin-bottom: 2em; - margin-bottom: var(--space-lg) -} - -.margin-y-xl { - margin-top: 3.25em; - margin-top: var(--space-xl); - margin-bottom: 3.25em; - margin-bottom: var(--space-xl) -} - -.margin-y-xxl { - margin-top: 5.25em; - margin-top: var(--space-xxl); - margin-bottom: 5.25em; - margin-bottom: var(--space-xxl) -} - -.margin-y-xxxl { - margin-top: 8.5em; - margin-top: var(--space-xxxl); - margin-bottom: 8.5em; - margin-bottom: var(--space-xxxl) -} - -.margin-y-xxxxl { - margin-top: 13.75em; - margin-top: var(--space-xxxxl); - margin-bottom: 13.75em; - margin-bottom: var(--space-xxxxl) -} - -.margin-y-auto { - margin-top: auto; - margin-bottom: auto -} - -@media not all and (min-width: 32rem) { - .has-margin\@xs { - margin: 0 !important - } -} - -@media not all and (min-width: 48rem) { - .has-margin\@sm { - margin: 0 !important - } -} - -@media not all and (min-width: 64rem) { - .has-margin\@md { - margin: 0 !important - } -} - -@media not all and (min-width: 80rem) { - .has-margin\@lg { - margin: 0 !important - } -} - -@media not all and (min-width: 90rem) { - .has-margin\@xl { - margin: 0 !important - } -} - -.padding-md { - padding: 1.25em; - padding: var(--space-md) -} - -.padding-xxxxs { - padding: 0.125em; - padding: var(--space-xxxxs) -} - -.padding-xxxs { - padding: 0.25em; - padding: var(--space-xxxs) -} - -.padding-xxs { - padding: 0.375em; - padding: var(--space-xxs) -} - -.padding-xs { - padding: 0.5em; - padding: var(--space-xs) -} - -.padding-sm { - padding: 0.75em; - padding: var(--space-sm) -} - -.padding-lg { - padding: 2em; - padding: var(--space-lg) -} - -.padding-xl { - padding: 3.25em; - padding: var(--space-xl) -} - -.padding-xxl { - padding: 5.25em; - padding: var(--space-xxl) -} - -.padding-xxxl { - padding: 8.5em; - padding: var(--space-xxxl) -} - -.padding-xxxxl { - padding: 13.75em; - padding: var(--space-xxxxl) -} - -.padding-component { - padding: 1.25em; - padding: var(--component-padding) -} - -.padding-top-md { - padding-top: 1.25em; - padding-top: var(--space-md) -} - -.padding-top-xxxxs { - padding-top: 0.125em; - padding-top: var(--space-xxxxs) -} - -.padding-top-xxxs { - padding-top: 0.25em; - padding-top: var(--space-xxxs) -} - -.padding-top-xxs { - padding-top: 0.375em; - padding-top: var(--space-xxs) -} - -.padding-top-xs { - padding-top: 0.5em; - padding-top: var(--space-xs) -} - -.padding-top-sm { - padding-top: 0.75em; - padding-top: var(--space-sm) -} - -.padding-top-lg { - padding-top: 2em; - padding-top: var(--space-lg) -} - -.padding-top-xl { - padding-top: 3.25em; - padding-top: var(--space-xl) -} - -.padding-top-xxl { - padding-top: 5.25em; - padding-top: var(--space-xxl) -} - -.padding-top-xxxl { - padding-top: 8.5em; - padding-top: var(--space-xxxl) -} - -.padding-top-xxxxl { - padding-top: 13.75em; - padding-top: var(--space-xxxxl) -} - -.padding-top-component { - padding-top: 1.25em; - padding-top: var(--component-padding) -} - -.padding-bottom-md { - padding-bottom: 1.25em; - padding-bottom: var(--space-md) -} - -.padding-bottom-xxxxs { - padding-bottom: 0.125em; - padding-bottom: var(--space-xxxxs) -} - -.padding-bottom-xxxs { - padding-bottom: 0.25em; - padding-bottom: var(--space-xxxs) -} - -.padding-bottom-xxs { - padding-bottom: 0.375em; - padding-bottom: var(--space-xxs) -} - -.padding-bottom-xs { - padding-bottom: 0.5em; - padding-bottom: var(--space-xs) -} - -.padding-bottom-sm { - padding-bottom: 0.75em; - padding-bottom: var(--space-sm) -} - -.padding-bottom-lg { - padding-bottom: 2em; - padding-bottom: var(--space-lg) -} - -.padding-bottom-xl { - padding-bottom: 3.25em; - padding-bottom: var(--space-xl) -} - -.padding-bottom-xxl { - padding-bottom: 5.25em; - padding-bottom: var(--space-xxl) -} - -.padding-bottom-xxxl { - padding-bottom: 8.5em; - padding-bottom: var(--space-xxxl) -} - -.padding-bottom-xxxxl { - padding-bottom: 13.75em; - padding-bottom: var(--space-xxxxl) -} - -.padding-bottom-component { - padding-bottom: 1.25em; - padding-bottom: var(--component-padding) -} - -.padding-right-md { - padding-right: 1.25em; - padding-right: var(--space-md) -} - -.padding-right-xxxxs { - padding-right: 0.125em; - padding-right: var(--space-xxxxs) -} - -.padding-right-xxxs { - padding-right: 0.25em; - padding-right: var(--space-xxxs) -} - -.padding-right-xxs { - padding-right: 0.375em; - padding-right: var(--space-xxs) -} - -.padding-right-xs { - padding-right: 0.5em; - padding-right: var(--space-xs) -} - -.padding-right-sm { - padding-right: 0.75em; - padding-right: var(--space-sm) -} - -.padding-right-lg { - padding-right: 2em; - padding-right: var(--space-lg) -} - -.padding-right-xl { - padding-right: 3.25em; - padding-right: var(--space-xl) -} - -.padding-right-xxl { - padding-right: 5.25em; - padding-right: var(--space-xxl) -} - -.padding-right-xxxl { - padding-right: 8.5em; - padding-right: var(--space-xxxl) -} - -.padding-right-xxxxl { - padding-right: 13.75em; - padding-right: var(--space-xxxxl) -} - -.padding-right-component { - padding-right: 1.25em; - padding-right: var(--component-padding) -} - -.padding-left-md { - padding-left: 1.25em; - padding-left: var(--space-md) -} - -.padding-left-xxxxs { - padding-left: 0.125em; - padding-left: var(--space-xxxxs) -} - -.padding-left-xxxs { - padding-left: 0.25em; - padding-left: var(--space-xxxs) -} - -.padding-left-xxs { - padding-left: 0.375em; - padding-left: var(--space-xxs) -} - -.padding-left-xs { - padding-left: 0.5em; - padding-left: var(--space-xs) -} - -.padding-left-sm { - padding-left: 0.75em; - padding-left: var(--space-sm) -} - -.padding-left-lg { - padding-left: 2em; - padding-left: var(--space-lg) -} - -.padding-left-xl { - padding-left: 3.25em; - padding-left: var(--space-xl) -} - -.padding-left-xxl { - padding-left: 5.25em; - padding-left: var(--space-xxl) -} - -.padding-left-xxxl { - padding-left: 8.5em; - padding-left: var(--space-xxxl) -} - -.padding-left-xxxxl { - padding-left: 13.75em; - padding-left: var(--space-xxxxl) -} - -.padding-left-component { - padding-left: 1.25em; - padding-left: var(--component-padding) -} - -.padding-x-md { - padding-left: 1.25em; - padding-left: var(--space-md); - padding-right: 1.25em; - padding-right: var(--space-md) -} - -.padding-x-xxxxs { - padding-left: 0.125em; - padding-left: var(--space-xxxxs); - padding-right: 0.125em; - padding-right: var(--space-xxxxs) -} - -.padding-x-xxxs { - padding-left: 0.25em; - padding-left: var(--space-xxxs); - padding-right: 0.25em; - padding-right: var(--space-xxxs) -} - -.padding-x-xxs { - padding-left: 0.375em; - padding-left: var(--space-xxs); - padding-right: 0.375em; - padding-right: var(--space-xxs) -} - -.padding-x-xs { - padding-left: 0.5em; - padding-left: var(--space-xs); - padding-right: 0.5em; - padding-right: var(--space-xs) -} - -.padding-x-sm { - padding-left: 0.75em; - padding-left: var(--space-sm); - padding-right: 0.75em; - padding-right: var(--space-sm) -} - -.padding-x-lg { - padding-left: 2em; - padding-left: var(--space-lg); - padding-right: 2em; - padding-right: var(--space-lg) -} - -.padding-x-xl { - padding-left: 3.25em; - padding-left: var(--space-xl); - padding-right: 3.25em; - padding-right: var(--space-xl) -} - -.padding-x-xxl { - padding-left: 5.25em; - padding-left: var(--space-xxl); - padding-right: 5.25em; - padding-right: var(--space-xxl) -} - -.padding-x-xxxl { - padding-left: 8.5em; - padding-left: var(--space-xxxl); - padding-right: 8.5em; - padding-right: var(--space-xxxl) -} - -.padding-x-xxxxl { - padding-left: 13.75em; - padding-left: var(--space-xxxxl); - padding-right: 13.75em; - padding-right: var(--space-xxxxl) -} - -.padding-x-component { - padding-left: 1.25em; - padding-left: var(--component-padding); - padding-right: 1.25em; - padding-right: var(--component-padding) -} - -.padding-y-md { - padding-top: 1.25em; - padding-top: var(--space-md); - padding-bottom: 1.25em; - padding-bottom: var(--space-md) -} - -.padding-y-xxxxs { - padding-top: 0.125em; - padding-top: var(--space-xxxxs); - padding-bottom: 0.125em; - padding-bottom: var(--space-xxxxs) -} - -.padding-y-xxxs { - padding-top: 0.25em; - padding-top: var(--space-xxxs); - padding-bottom: 0.25em; - padding-bottom: var(--space-xxxs) -} - -.padding-y-xxs { - padding-top: 0.375em; - padding-top: var(--space-xxs); - padding-bottom: 0.375em; - padding-bottom: var(--space-xxs) -} - -.padding-y-xs { - padding-top: 0.5em; - padding-top: var(--space-xs); - padding-bottom: 0.5em; - padding-bottom: var(--space-xs) -} - -.padding-y-sm { - padding-top: 0.75em; - padding-top: var(--space-sm); - padding-bottom: 0.75em; - padding-bottom: var(--space-sm) -} - -.padding-y-lg { - padding-top: 2em; - padding-top: var(--space-lg); - padding-bottom: 2em; - padding-bottom: var(--space-lg) -} - -.padding-y-xl { - padding-top: 3.25em; - padding-top: var(--space-xl); - padding-bottom: 3.25em; - padding-bottom: var(--space-xl) -} - -.padding-y-xxl { - padding-top: 5.25em; - padding-top: var(--space-xxl); - padding-bottom: 5.25em; - padding-bottom: var(--space-xxl) -} - -.padding-y-xxxl { - padding-top: 8.5em; - padding-top: var(--space-xxxl); - padding-bottom: 8.5em; - padding-bottom: var(--space-xxxl) -} - -.padding-y-xxxxl { - padding-top: 13.75em; - padding-top: var(--space-xxxxl); - padding-bottom: 13.75em; - padding-bottom: var(--space-xxxxl) -} - -.padding-y-component { - padding-top: 1.25em; - padding-top: var(--component-padding); - padding-bottom: 1.25em; - padding-bottom: var(--component-padding) -} - -@media not all and (min-width: 32rem) { - .has-padding\@xs { - padding: 0 !important - } -} - -@media not all and (min-width: 48rem) { - .has-padding\@sm { - padding: 0 !important - } -} - -@media not all and (min-width: 64rem) { - .has-padding\@md { - padding: 0 !important - } -} - -@media not all and (min-width: 80rem) { - .has-padding\@lg { - padding: 0 !important - } -} - -@media not all and (min-width: 90rem) { - .has-padding\@xl { - padding: 0 !important - } -} - -.truncate { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap -} - -.text-replace { - overflow: hidden; - color: transparent; - text-indent: 100%; - white-space: nowrap -} - -.text-center { - text-align: center -} - -.text-left { - text-align: left -} - -.text-right { - text-align: right -} - -@media (min-width: 32rem) { - .text-center\@xs { - text-align: center - } - .text-left\@xs { - text-align: left - } - .text-right\@xs { - text-align: right - } -} - -@media (min-width: 48rem) { - .text-center\@sm { - text-align: center - } - .text-left\@sm { - text-align: left - } - .text-right\@sm { - text-align: right - } -} - -@media (min-width: 64rem) { - .text-center\@md { - text-align: center - } - .text-left\@md { - text-align: left - } - .text-right\@md { - text-align: right - } -} - -@media (min-width: 80rem) { - .text-center\@lg { - text-align: center - } - .text-left\@lg { - text-align: left - } - .text-right\@lg { - text-align: right - } -} - -@media (min-width: 90rem) { - .text-center\@xl { - text-align: center - } - .text-left\@xl { - text-align: left - } - .text-right\@xl { - text-align: right - } -} - -.color-inherit { - color: inherit -} - -.color-contrast-medium { - color: hsl(240, 1%, 48%); - color: var(--color-contrast-medium, #79797c) -} - -.color-contrast-high { - color: hsl(240, 4%, 20%); - color: var(--color-contrast-high, #313135) -} - -.color-contrast-higher { - color: hsl(240, 8%, 12%); - color: var(--color-contrast-higher, #1c1c21) -} - -.color-primary { - color: hsl(220, 90%, 56%); - color: var(--color-primary, #2a6df4) -} - -.color-accent { - color: hsl(355, 90%, 61%); - color: var(--color-accent, #f54251) -} - -.color-success { - color: hsl(94, 48%, 56%); - color: var(--color-success, #88c559) -} - -.color-warning { - color: hsl(46, 100%, 61%); - color: var(--color-warning, #ffd138) -} - -.color-error { - color: hsl(355, 90%, 61%); - color: var(--color-error, #f54251) -} - -.width-100\% { - width: 100% -} - -.height-100\% { - height: 100% -} - -.media-wrapper { - position: relative; - height: 0; - padding-bottom: 56.25% -} - -.media-wrapper iframe, .media-wrapper video, .media-wrapper img { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100% -} - -.media-wrapper video, .media-wrapper img { - -o-object-fit: cover; - object-fit: cover -} - -.media-wrapper--4\:3 { - padding-bottom: 75% -} - -:root, [data-theme="default"] { - --color-primary-darker: hsl(220, 90%, 36%); - --color-primary-darker-h: 220; - --color-primary-darker-s: 90%; - --color-primary-darker-l: 36%; - --color-primary-dark: hsl(220, 90%, 46%); - --color-primary-dark-h: 220; - --color-primary-dark-s: 90%; - --color-primary-dark-l: 46%; - --color-primary: hsl(220, 90%, 56%); - --color-primary-h: 220; - --color-primary-s: 90%; - --color-primary-l: 56%; - --color-primary-light: hsl(220, 90%, 66%); - --color-primary-light-h: 220; - --color-primary-light-s: 90%; - --color-primary-light-l: 66%; - --color-primary-lighter: hsl(220, 90%, 76%); - --color-primary-lighter-h: 220; - --color-primary-lighter-s: 90%; - --color-primary-lighter-l: 76%; - --color-accent-darker: hsl(355, 90%, 41%); - --color-accent-darker-h: 355; - --color-accent-darker-s: 90%; - --color-accent-darker-l: 41%; - --color-accent-dark: hsl(355, 90%, 51%); - --color-accent-dark-h: 355; - --color-accent-dark-s: 90%; - --color-accent-dark-l: 51%; - --color-accent: hsl(355, 90%, 61%); - --color-accent-h: 355; - --color-accent-s: 90%; - --color-accent-l: 61%; - --color-accent-light: hsl(355, 90%, 71%); - --color-accent-light-h: 355; - --color-accent-light-s: 90%; - --color-accent-light-l: 71%; - --color-accent-lighter: hsl(355, 90%, 81%); - --color-accent-lighter-h: 355; - --color-accent-lighter-s: 90%; - --color-accent-lighter-l: 81%; - --color-black: hsl(240, 8%, 12%); - --color-black-h: 240; - --color-black-s: 8%; - --color-black-l: 12%; - --color-white: hsl(0, 0%, 100%); - --color-white-h: 0; - --color-white-s: 0%; - --color-white-l: 100%; - --color-success-darker: hsl(94, 48%, 36%); - --color-success-darker-h: 94; - --color-success-darker-s: 48%; - --color-success-darker-l: 36%; - --color-success-dark: hsl(94, 48%, 46%); - --color-success-dark-h: 94; - --color-success-dark-s: 48%; - --color-success-dark-l: 46%; - --color-success: hsl(94, 48%, 56%); - --color-success-h: 94; - --color-success-s: 48%; - --color-success-l: 56%; - --color-success-light: hsl(94, 48%, 66%); - --color-success-light-h: 94; - --color-success-light-s: 48%; - --color-success-light-l: 66%; - --color-success-lighter: hsl(94, 48%, 76%); - --color-success-lighter-h: 94; - --color-success-lighter-s: 48%; - --color-success-lighter-l: 76%; - --color-error-darker: hsl(355, 90%, 41%); - --color-error-darker-h: 355; - --color-error-darker-s: 90%; - --color-error-darker-l: 41%; - --color-error-dark: hsl(355, 90%, 51%); - --color-error-dark-h: 355; - --color-error-dark-s: 90%; - --color-error-dark-l: 51%; - --color-error: hsl(355, 90%, 61%); - --color-error-h: 355; - --color-error-s: 90%; - --color-error-l: 61%; - --color-error-light: hsl(355, 90%, 71%); - --color-error-light-h: 355; - --color-error-light-s: 90%; - --color-error-light-l: 71%; - --color-error-lighter: hsl(355, 90%, 81%); - --color-error-lighter-h: 355; - --color-error-lighter-s: 90%; - --color-error-lighter-l: 81%; - --color-warning-darker: hsl(46, 100%, 41%); - --color-warning-darker-h: 46; - --color-warning-darker-s: 100%; - --color-warning-darker-l: 41%; - --color-warning-dark: hsl(46, 100%, 51%); - --color-warning-dark-h: 46; - --color-warning-dark-s: 100%; - --color-warning-dark-l: 51%; - --color-warning: hsl(46, 100%, 61%); - --color-warning-h: 46; - --color-warning-s: 100%; - --color-warning-l: 61%; - --color-warning-light: hsl(46, 100%, 71%); - --color-warning-light-h: 46; - --color-warning-light-s: 100%; - --color-warning-light-l: 71%; - --color-warning-lighter: hsl(46, 100%, 81%); - --color-warning-lighter-h: 46; - --color-warning-lighter-s: 100%; - --color-warning-lighter-l: 81%; - --color-bg: hsl(0, 0%, 100%); - --color-bg-h: 0; - --color-bg-s: 0%; - --color-bg-l: 100%; - --color-contrast-lower: hsl(0, 0%, 95%); - --color-contrast-lower-h: 0; - --color-contrast-lower-s: 0%; - --color-contrast-lower-l: 95%; - --color-contrast-low: hsl(240, 1%, 83%); - --color-contrast-low-h: 240; - --color-contrast-low-s: 1%; - --color-contrast-low-l: 83%; - --color-contrast-medium: hsl(240, 1%, 48%); - --color-contrast-medium-h: 240; - --color-contrast-medium-s: 1%; - --color-contrast-medium-l: 48%; - --color-contrast-high: hsl(240, 4%, 20%); - --color-contrast-high-h: 240; - --color-contrast-high-s: 4%; - --color-contrast-high-l: 20%; - --color-contrast-higher: hsl(240, 8%, 12%); - --color-contrast-higher-h: 240; - --color-contrast-higher-s: 8%; - --color-contrast-higher-l: 12% -} - -@supports (--css: variables) { - @media (min-width: 64rem) { - :root { - --space-unit: 1.25em - } - } -} - -:root { - --radius: 0.25em -} - -:root { - --font-primary: sans-serif; - --text-base-size: 1em; - --text-scale-ratio: 1.2; - --text-xs: calc(1em/var(--text-scale-ratio)/var(--text-scale-ratio)); - --text-sm: calc(var(--text-xs)*var(--text-scale-ratio)); - --text-md: calc(var(--text-sm)*var(--text-scale-ratio)*var(--text-scale-ratio)); - --text-lg: calc(var(--text-md)*var(--text-scale-ratio)); - --text-xl: calc(var(--text-lg)*var(--text-scale-ratio)); - --text-xxl: calc(var(--text-xl)*var(--text-scale-ratio)); - --text-xxxl: calc(var(--text-xxl)*var(--text-scale-ratio)); - --body-line-height: 1.4; - --heading-line-height: 1.2; - --font-primary-capital-letter: 1 -} - -@supports (--css: variables) { - @media (min-width: 64rem) { - :root { - --text-base-size: 1.25em; - --text-scale-ratio: 1.25 - } - } -} - -mark { - background-color: hsla(355, 90%, 61%, 0.2); - background-color: hsla(var(--color-accent-h), var(--color-accent-s), var(--color-accent-l), 0.2); - color: inherit -} - -.text-component { - --line-height-multiplier: 1; - --text-vspace-multiplier: 1 -} - -.text-component blockquote { - padding-left: 1em; - border-left: 4px solid hsl(240, 1%, 83%); - border-left: 4px solid var(--color-contrast-low) -} - -.text-component hr { - background: hsl(240, 1%, 83%); - background: var(--color-contrast-low); - height: 1px -} - -.text-component figcaption { - font-size: 0.83333em; - font-size: var(--text-sm); - color: hsl(240, 1%, 48%); - color: var(--color-contrast-medium) -} - -.article.text-component { - --line-height-multiplier: 1.13; - --text-vspace-multiplier: 1.2 -} - -:root { - --btn-font-size: 1em; - --btn-font-size-sm: calc(var(--btn-font-size) - 0.2em); - --btn-font-size-md: calc(var(--btn-font-size) + 0.2em); - --btn-font-size-lg: calc(var(--btn-font-size) + 0.4em); - --btn-radius: 0.25em; - --btn-padding-x: var(--space-sm); - --btn-padding-y: var(--space-xs) -} - -.btn { - --color-shadow: hsla(240, 8%, 12%, 0.15); - --color-shadow: hsla(var(--color-black-h), var(--color-black-s), var(--color-black-l), 0.15); - box-shadow: 0 4px 16px hsla(240, 8%, 12%, 0.15); - box-shadow: 0 4px 16px hsla(var(--color-black-h), var(--color-black-s), var(--color-black-l), 0.15); - cursor: pointer -} - -.btn--primary { - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale -} - -.btn--accent { - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale -} - -.btn--disabled { - opacity: 0.6 -} - -:root { - --form-control-padding-x: var(--space-sm); - --form-control-padding-y: var(--space-xs); - --form-control-radius: 0.25em -} - -.form-control { - border: 2px solid hsl(240, 1%, 83%); - border: 2px solid var(--color-contrast-low) -} - -.form-control:focus { - outline: none; - border-color: hsl(220, 90%, 56%); - border-color: var(--color-primary); - --color-shadow: hsla(220, 90%, 56%, 0.2); - --color-shadow: hsla(var(--color-primary-h), var(--color-primary-s), var(--color-primary-l), 0.2); - box-shadow: undefined; - box-shadow: 0 0 0 3px var(--color-shadow) -} - -.form-control:focus:focus { - box-shadow: 0 0 0 3px hsla(220, 90%, 56%, 0.2); - box-shadow: 0 0 0 3px var(--color-shadow) -} - -.form-control[aria-invalid="true"] { - border-color: hsl(355, 90%, 61%); - border-color: var(--color-error) -} - -.form-control[aria-invalid="true"]:focus { - --color-shadow: hsla(355, 90%, 61%, 0.2); - --color-shadow: hsla(var(--color-error-h), var(--color-error-s), var(--color-error-l), 0.2); - box-shadow: undefined; - box-shadow: 0 0 0 3px var(--color-shadow) -} - -.form-control[aria-invalid="true"]:focus:focus { - box-shadow: 0 0 0 3px hsla(355, 90%, 61%, 0.2); - box-shadow: 0 0 0 3px var(--color-shadow) -} - -.form-label { - font-size: 0.83333em; - font-size: var(--text-sm) -} - -:root { - --cd-color-1: hsl(206, 21%, 24%); - --cd-color-1-h: 206; - --cd-color-1-s: 21%; - --cd-color-1-l: 24%; - --cd-color-2: hsl(205, 38%, 89%); - --cd-color-2-h: 205; - --cd-color-2-s: 38%; - --cd-color-2-l: 89%; - --cd-color-3: hsl(207, 10%, 55%); - --cd-color-3-h: 207; - --cd-color-3-s: 10%; - --cd-color-3-l: 55%; - --cd-color-4: hsl(111, 51%, 60%); - --cd-color-4-h: 111; - --cd-color-4-s: 51%; - --cd-color-4-l: 60%; - --cd-color-5: hsl(356, 53%, 49%); - --cd-color-5-h: 356; - --cd-color-5-s: 53%; - --cd-color-5-l: 49%; - --cd-color-6: hsl(47, 85%, 61%); - --cd-color-6-h: 47; - --cd-color-6-s: 85%; - --cd-color-6-l: 61%; - --cd-header-height: 200px; - --font-primary: 'Droid Serif', serif; - --font-secondary: 'Open Sans', sans-serif -} - -@supports (--css: variables) { - @media (min-width: 64rem) { - :root { - --cd-header-height: 300px - } - } -} - -.cd-main-header { - height: 200px; - height: var(--cd-header-height); - background: hsl(206, 21%, 24%); - background: var(--cd-color-1); - color: hsl(0, 0%, 100%); - color: var(--color-white); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale -} - -.cd-main-header h1 { - font-family: 'Open Sans', sans-serif; - font-family: var(--font-secondary); - color: inherit -} - -.cd-timeline { - overflow: hidden; - padding: 2em 0; - padding: var(--space-lg) 0; - color: hsl(207, 10%, 55%); - color: var(--cd-color-3); - background-color: hsl(205, 38%, 93.45%); - background-color: hsl(var(--cd-color-2-h), var(--cd-color-2-s), calc(var(--cd-color-2-l)*1.05)); - font-family: 'Droid Serif', serif; - font-family: var(--font-primary) -} - -.cd-timeline h2 { - font-family: 'Open Sans', sans-serif; - font-family: var(--font-secondary); - font-weight: 700 -} - -.cd-timeline__container { - position: relative; - padding: 1.25em 0; - padding: var(--space-md) 0 -} - -.cd-timeline__container::before { - content: ''; - position: absolute; - top: 0; - left: 18px; - height: 100%; - width: 4px; - background: hsl(205, 38%, 89%); - background: var(--cd-color-2) -} - -@media (min-width: 64rem) { - .cd-timeline__container::before { - left: 50%; - -webkit-transform: translateX(-50%); - -ms-transform: translateX(-50%); - transform: translateX(-50%) - } -} - -.cd-timeline__block { - display: -ms-flexbox; - display: flex; - position: relative; - z-index: 1; - margin-bottom: 2em; - margin-bottom: var(--space-lg) -} - -.cd-timeline__block:last-child { - margin-bottom: 0 -} - -@media (min-width: 64rem) { - .cd-timeline__block:nth-child(even) { - -ms-flex-direction: row-reverse; - flex-direction: row-reverse - } -} - -.cd-timeline__img { - display: -ms-flexbox; - display: flex; - -ms-flex-pack: center; - justify-content: center; - -ms-flex-align: center; - align-items: center; - -ms-flex-negative: 0; - flex-shrink: 0; - width: 40px; - height: 40px; - border-radius: 50%; - box-shadow: 0 0 0 4px hsl(0, 0%, 100%), inset 0 2px 0 rgba(0, 0, 0, 0.08), 0 3px 0 4px rgba(0, 0, 0, 0.05); - box-shadow: 0 0 0 4px var(--color-white), inset 0 2px 0 rgba(0, 0, 0, 0.08), 0 3px 0 4px rgba(0, 0, 0, 0.05) -} - -.cd-timeline__img img { - width: 24px; - height: 24px -} - -@media (min-width: 64rem) { - .cd-timeline__img { - width: 60px; - height: 60px; - -ms-flex-order: 1; - order: 1; - margin-left: calc(5% - 30px); - will-change: transform - } - .cd-timeline__block:nth-child(even) .cd-timeline__img { - margin-right: calc(5% - 30px) - } -} - -.cd-timeline__img--picture { - background-color: hsl(111, 51%, 60%); - background-color: var(--cd-color-4) -} - -.cd-timeline__img--movie { - background-color: hsl(356, 53%, 49%); - background-color: var(--cd-color-5) -} - -.cd-timeline__img--location { - background-color: hsl(47, 85%, 61%); - background-color: var(--cd-color-6) -} - -.cd-timeline__content { - -ms-flex-positive: 1; - flex-grow: 1; - position: relative; - margin-left: 1.25em; - margin-left: var(--space-md); - background: hsl(0, 0%, 100%); - background: var(--color-white); - border-radius: 0.25em; - border-radius: var(--radius-md); - padding: 1.25em; - padding: var(--space-md); - box-shadow: 0 3px 0 hsl(205, 38%, 89%); - box-shadow: 0 3px 0 var(--cd-color-2) -} - -.cd-timeline__content::before { - content: ''; - position: absolute; - top: 16px; - right: 100%; - width: 0; - height: 0; - border: 7px solid transparent; - border-right-color: hsl(0, 0%, 100%); - border-right-color: var(--color-white) -} - -.cd-timeline__content h2 { - color: hsl(206, 21%, 24%); - color: var(--cd-color-1) -} - -@media (min-width: 64rem) { - .cd-timeline__content { - width: 45%; - -ms-flex-positive: 0; - flex-grow: 0; - will-change: transform; - margin: 0; - font-size: 0.8em; - --line-height-multiplier: 1.2 - } - .cd-timeline__content::before { - top: 24px - } - .cd-timeline__block:nth-child(odd) .cd-timeline__content::before { - right: auto; - left: 100%; - width: 0; - height: 0; - border: 7px solid transparent; - border-left-color: hsl(0, 0%, 100%); - border-left-color: var(--color-white) - } -} - -.cd-timeline__date { - color: hsla(207, 10%, 55%, 0.7); - color: hsla(var(--cd-color-3-h), var(--cd-color-3-s), var(--cd-color-3-l), 0.7) -} - -@media (min-width: 64rem) { - .cd-timeline__date { - position: absolute; - width: 100%; - left: 120%; - top: 20px - } - .cd-timeline__block:nth-child(even) .cd-timeline__date { - left: auto; - right: 120%; - text-align: right - } -} - -@media (min-width: 64rem) { - .cd-timeline__img--hidden, .cd-timeline__content--hidden { - visibility: hidden - } - .cd-timeline__img--bounce-in { - -webkit-animation: cd-bounce-1 0.6s; - animation: cd-bounce-1 0.6s - } - .cd-timeline__content--bounce-in { - -webkit-animation: cd-bounce-2 0.6s; - animation: cd-bounce-2 0.6s - } - .cd-timeline__block:nth-child(even) .cd-timeline__content--bounce-in { - -webkit-animation-name: cd-bounce-2-inverse; - animation-name: cd-bounce-2-inverse - } -} - -@-webkit-keyframes cd-bounce-1 { - 0% { - opacity: 0; - -webkit-transform: scale(0.5); - transform: scale(0.5) - } - 60% { - opacity: 1; - -webkit-transform: scale(1.2); - transform: scale(1.2) - } - 100% { - -webkit-transform: scale(1); - transform: scale(1) - } -} - -@keyframes cd-bounce-1 { - 0% { - opacity: 0; - -webkit-transform: scale(0.5); - transform: scale(0.5) - } - 60% { - opacity: 1; - -webkit-transform: scale(1.2); - transform: scale(1.2) - } - 100% { - -webkit-transform: scale(1); - transform: scale(1) - } -} - -@-webkit-keyframes cd-bounce-2 { - 0% { - opacity: 0; - -webkit-transform: translateX(-100px); - transform: translateX(-100px) - } - 60% { - opacity: 1; - -webkit-transform: translateX(20px); - transform: translateX(20px) - } - 100% { - -webkit-transform: translateX(0); - transform: translateX(0) - } -} - -@keyframes cd-bounce-2 { - 0% { - opacity: 0; - -webkit-transform: translateX(-100px); - transform: translateX(-100px) - } - 60% { - opacity: 1; - -webkit-transform: translateX(20px); - transform: translateX(20px) - } - 100% { - -webkit-transform: translateX(0); - transform: translateX(0) - } -} - -@-webkit-keyframes cd-bounce-2-inverse { - 0% { - opacity: 0; - -webkit-transform: translateX(100px); - transform: translateX(100px) - } - 60% { - opacity: 1; - -webkit-transform: translateX(-20px); - transform: translateX(-20px) - } - 100% { - -webkit-transform: translateX(0); - transform: translateX(0) - } -} - -@keyframes cd-bounce-2-inverse { - 0% { - opacity: 0; - -webkit-transform: translateX(100px); - transform: translateX(100px) - } - 60% { - opacity: 1; - -webkit-transform: translateX(-20px); - transform: translateX(-20px) - } - 100% { - -webkit-transform: translateX(0); - transform: translateX(0) - } -} diff --git a/pydis_site/static/css/home/timeline.css b/pydis_site/static/css/home/timeline.css new file mode 100644 index 00000000..dbe49755 --- /dev/null +++ b/pydis_site/static/css/home/timeline.css @@ -0,0 +1,3674 @@ +body { + background-color: hsl(0, 0%, 100%); + background-color: var(--color-bg, white) +} + +h2 { + font-size: 2em; +} + +article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section, main, form legend { + display: block +} + +ol, ul { + list-style: none +} + +blockquote, q { + quotes: none +} + +button, input, textarea, select { + margin: 0 +} + +.btn, .form-control, .link, .reset { + background-color: transparent; + padding: 0; + border: 0; + border-radius: 0; + color: inherit; + line-height: inherit; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none +} + +select.form-control::-ms-expand { + display: none +} + +textarea { + resize: vertical; + overflow: auto; + vertical-align: top +} + +input::-ms-clear { + display: none +} + +table { + border-collapse: collapse; + border-spacing: 0 +} + +img, video, svg { + max-width: 100% +} + +[data-theme] { + background-color: hsl(0, 0%, 100%); + background-color: var(--color-bg, #fff); + color: hsl(240, 4%, 20%); + color: var(--color-contrast-high, #313135) +} + +:root { + --space-unit: 1em; + --space-xxxxs: calc(0.125*var(--space-unit)); + --space-xxxs: calc(0.25*var(--space-unit)); + --space-xxs: calc(0.375*var(--space-unit)); + --space-xs: calc(0.5*var(--space-unit)); + --space-sm: calc(0.75*var(--space-unit)); + --space-md: calc(1.25*var(--space-unit)); + --space-lg: calc(2*var(--space-unit)); + --space-xl: calc(3.25*var(--space-unit)); + --space-xxl: calc(5.25*var(--space-unit)); + --space-xxxl: calc(8.5*var(--space-unit)); + --space-xxxxl: calc(13.75*var(--space-unit)); + --component-padding: var(--space-md) +} + +:root { + --max-width-xxs: 32rem; + --max-width-xs: 38rem; + --max-width-sm: 48rem; + --max-width-md: 64rem; + --max-width-lg: 80rem; + --max-width-xl: 90rem; + --max-width-xxl: 120rem +} + +.container { + width: calc(100% - 1.25em); + width: calc(100% - 2*var(--component-padding)); + margin-left: auto; + margin-right: auto +} + +.max-width-xxs { + max-width: 32rem; + max-width: var(--max-width-xxs) +} + +.max-width-xs { + max-width: 38rem; + max-width: var(--max-width-xs) +} + +.max-width-sm { + max-width: 48rem; + max-width: var(--max-width-sm) +} + +.max-width-md { + max-width: 64rem; + max-width: var(--max-width-md) +} + +.max-width-lg { + max-width: 80rem; + max-width: var(--max-width-lg) +} + +.max-width-xl { + max-width: 90rem; + max-width: var(--max-width-xl) +} + +.max-width-xxl { + max-width: 120rem; + max-width: var(--max-width-xxl) +} + +.max-width-adaptive-sm { + max-width: 38rem; + max-width: var(--max-width-xs) +} + +@media (min-width: 64rem) { + .max-width-adaptive-sm { + max-width: 48rem; + max-width: var(--max-width-sm) + } +} + +.max-width-adaptive-md { + max-width: 38rem; + max-width: var(--max-width-xs) +} + +@media (min-width: 64rem) { + .max-width-adaptive-md { + max-width: 64rem; + max-width: var(--max-width-md) + } +} + +.max-width-adaptive, .max-width-adaptive-lg { + max-width: 38rem; + max-width: var(--max-width-xs) +} + +@media (min-width: 64rem) { + .max-width-adaptive, .max-width-adaptive-lg { + max-width: 64rem; + max-width: var(--max-width-md) + } +} + +@media (min-width: 90rem) { + .max-width-adaptive, .max-width-adaptive-lg { + max-width: 80rem; + max-width: var(--max-width-lg) + } +} + +.max-width-adaptive-xl { + max-width: 38rem; + max-width: var(--max-width-xs) +} + +@media (min-width: 64rem) { + .max-width-adaptive-xl { + max-width: 64rem; + max-width: var(--max-width-md) + } +} + +@media (min-width: 90rem) { + .max-width-adaptive-xl { + max-width: 90rem; + max-width: var(--max-width-xl) + } +} + +.grid { + --grid-gap: 0px; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap +} + +.grid>* { + -ms-flex-preferred-size: 100%; + flex-basis: 100% +} + +[class*="grid-gap"] { + margin-bottom: 1em * -1; + margin-bottom: calc(var(--grid-gap, 1em)*-1); + margin-right: 1em * -1; + margin-right: calc(var(--grid-gap, 1em)*-1) +} + +[class*="grid-gap"]>* { + margin-bottom: 1em; + margin-bottom: var(--grid-gap, 1em); + margin-right: 1em; + margin-right: var(--grid-gap, 1em) +} + +.grid-gap-xxxxs { + --grid-gap: var(--space-xxxxs) +} + +.grid-gap-xxxs { + --grid-gap: var(--space-xxxs) +} + +.grid-gap-xxs { + --grid-gap: var(--space-xxs) +} + +.grid-gap-xs { + --grid-gap: var(--space-xs) +} + +.grid-gap-sm { + --grid-gap: var(--space-sm) +} + +.grid-gap-md { + --grid-gap: var(--space-md) +} + +.grid-gap-lg { + --grid-gap: var(--space-lg) +} + +.grid-gap-xl { + --grid-gap: var(--space-xl) +} + +.grid-gap-xxl { + --grid-gap: var(--space-xxl) +} + +.grid-gap-xxxl { + --grid-gap: var(--space-xxxl) +} + +.grid-gap-xxxxl { + --grid-gap: var(--space-xxxxl) +} + +.col { + -ms-flex-positive: 1; + flex-grow: 1; + -ms-flex-preferred-size: 0; + flex-basis: 0; + max-width: 100% +} + +.col-1 { + -ms-flex-preferred-size: calc(8.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(8.33% - 0.01px - 1em); + flex-basis: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(8.33% - 0.01px - 1em); + max-width: calc(8.33% - 0.01px - var(--grid-gap, 1em)) +} + +.col-2 { + -ms-flex-preferred-size: calc(16.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(16.66% - 0.01px - 1em); + flex-basis: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(16.66% - 0.01px - 1em); + max-width: calc(16.66% - 0.01px - var(--grid-gap, 1em)) +} + +.col-3 { + -ms-flex-preferred-size: calc(25% - 0.01px - 1em); + -ms-flex-preferred-size: calc(25% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(25% - 0.01px - 1em); + flex-basis: calc(25% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(25% - 0.01px - 1em); + max-width: calc(25% - 0.01px - var(--grid-gap, 1em)) +} + +.col-4 { + -ms-flex-preferred-size: calc(33.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(33.33% - 0.01px - 1em); + flex-basis: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(33.33% - 0.01px - 1em); + max-width: calc(33.33% - 0.01px - var(--grid-gap, 1em)) +} + +.col-5 { + -ms-flex-preferred-size: calc(41.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(41.66% - 0.01px - 1em); + flex-basis: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(41.66% - 0.01px - 1em); + max-width: calc(41.66% - 0.01px - var(--grid-gap, 1em)) +} + +.col-6 { + -ms-flex-preferred-size: calc(50% - 0.01px - 1em); + -ms-flex-preferred-size: calc(50% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(50% - 0.01px - 1em); + flex-basis: calc(50% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(50% - 0.01px - 1em); + max-width: calc(50% - 0.01px - var(--grid-gap, 1em)) +} + +.col-7 { + -ms-flex-preferred-size: calc(58.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(58.33% - 0.01px - 1em); + flex-basis: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(58.33% - 0.01px - 1em); + max-width: calc(58.33% - 0.01px - var(--grid-gap, 1em)) +} + +.col-8 { + -ms-flex-preferred-size: calc(66.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(66.66% - 0.01px - 1em); + flex-basis: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(66.66% - 0.01px - 1em); + max-width: calc(66.66% - 0.01px - var(--grid-gap, 1em)) +} + +.col-9 { + -ms-flex-preferred-size: calc(75% - 0.01px - 1em); + -ms-flex-preferred-size: calc(75% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(75% - 0.01px - 1em); + flex-basis: calc(75% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(75% - 0.01px - 1em); + max-width: calc(75% - 0.01px - var(--grid-gap, 1em)) +} + +.col-10 { + -ms-flex-preferred-size: calc(83.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(83.33% - 0.01px - 1em); + flex-basis: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(83.33% - 0.01px - 1em); + max-width: calc(83.33% - 0.01px - var(--grid-gap, 1em)) +} + +.col-11 { + -ms-flex-preferred-size: calc(91.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(91.66% - 0.01px - 1em); + flex-basis: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(91.66% - 0.01px - 1em); + max-width: calc(91.66% - 0.01px - var(--grid-gap, 1em)) +} + +.col-12 { + -ms-flex-preferred-size: calc(100% - 0.01px - 1em); + -ms-flex-preferred-size: calc(100% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(100% - 0.01px - 1em); + flex-basis: calc(100% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(100% - 0.01px - 1em); + max-width: calc(100% - 0.01px - var(--grid-gap, 1em)) +} + +@media (min-width: 32rem) { + .col\@xs { + -ms-flex-positive: 1; + flex-grow: 1; + -ms-flex-preferred-size: 0; + flex-basis: 0; + max-width: 100% + } + .col-1\@xs { + -ms-flex-preferred-size: calc(8.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(8.33% - 0.01px - 1em); + flex-basis: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(8.33% - 0.01px - 1em); + max-width: calc(8.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-2\@xs { + -ms-flex-preferred-size: calc(16.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(16.66% - 0.01px - 1em); + flex-basis: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(16.66% - 0.01px - 1em); + max-width: calc(16.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-3\@xs { + -ms-flex-preferred-size: calc(25% - 0.01px - 1em); + -ms-flex-preferred-size: calc(25% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(25% - 0.01px - 1em); + flex-basis: calc(25% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(25% - 0.01px - 1em); + max-width: calc(25% - 0.01px - var(--grid-gap, 1em)) + } + .col-4\@xs { + -ms-flex-preferred-size: calc(33.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(33.33% - 0.01px - 1em); + flex-basis: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(33.33% - 0.01px - 1em); + max-width: calc(33.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-5\@xs { + -ms-flex-preferred-size: calc(41.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(41.66% - 0.01px - 1em); + flex-basis: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(41.66% - 0.01px - 1em); + max-width: calc(41.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-6\@xs { + -ms-flex-preferred-size: calc(50% - 0.01px - 1em); + -ms-flex-preferred-size: calc(50% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(50% - 0.01px - 1em); + flex-basis: calc(50% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(50% - 0.01px - 1em); + max-width: calc(50% - 0.01px - var(--grid-gap, 1em)) + } + .col-7\@xs { + -ms-flex-preferred-size: calc(58.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(58.33% - 0.01px - 1em); + flex-basis: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(58.33% - 0.01px - 1em); + max-width: calc(58.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-8\@xs { + -ms-flex-preferred-size: calc(66.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(66.66% - 0.01px - 1em); + flex-basis: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(66.66% - 0.01px - 1em); + max-width: calc(66.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-9\@xs { + -ms-flex-preferred-size: calc(75% - 0.01px - 1em); + -ms-flex-preferred-size: calc(75% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(75% - 0.01px - 1em); + flex-basis: calc(75% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(75% - 0.01px - 1em); + max-width: calc(75% - 0.01px - var(--grid-gap, 1em)) + } + .col-10\@xs { + -ms-flex-preferred-size: calc(83.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(83.33% - 0.01px - 1em); + flex-basis: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(83.33% - 0.01px - 1em); + max-width: calc(83.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-11\@xs { + -ms-flex-preferred-size: calc(91.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(91.66% - 0.01px - 1em); + flex-basis: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(91.66% - 0.01px - 1em); + max-width: calc(91.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-12\@xs { + -ms-flex-preferred-size: calc(100% - 0.01px - 1em); + -ms-flex-preferred-size: calc(100% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(100% - 0.01px - 1em); + flex-basis: calc(100% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(100% - 0.01px - 1em); + max-width: calc(100% - 0.01px - var(--grid-gap, 1em)) + } +} + +@media (min-width: 48rem) { + .col\@sm { + -ms-flex-positive: 1; + flex-grow: 1; + -ms-flex-preferred-size: 0; + flex-basis: 0; + max-width: 100% + } + .col-1\@sm { + -ms-flex-preferred-size: calc(8.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(8.33% - 0.01px - 1em); + flex-basis: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(8.33% - 0.01px - 1em); + max-width: calc(8.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-2\@sm { + -ms-flex-preferred-size: calc(16.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(16.66% - 0.01px - 1em); + flex-basis: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(16.66% - 0.01px - 1em); + max-width: calc(16.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-3\@sm { + -ms-flex-preferred-size: calc(25% - 0.01px - 1em); + -ms-flex-preferred-size: calc(25% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(25% - 0.01px - 1em); + flex-basis: calc(25% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(25% - 0.01px - 1em); + max-width: calc(25% - 0.01px - var(--grid-gap, 1em)) + } + .col-4\@sm { + -ms-flex-preferred-size: calc(33.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(33.33% - 0.01px - 1em); + flex-basis: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(33.33% - 0.01px - 1em); + max-width: calc(33.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-5\@sm { + -ms-flex-preferred-size: calc(41.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(41.66% - 0.01px - 1em); + flex-basis: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(41.66% - 0.01px - 1em); + max-width: calc(41.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-6\@sm { + -ms-flex-preferred-size: calc(50% - 0.01px - 1em); + -ms-flex-preferred-size: calc(50% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(50% - 0.01px - 1em); + flex-basis: calc(50% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(50% - 0.01px - 1em); + max-width: calc(50% - 0.01px - var(--grid-gap, 1em)) + } + .col-7\@sm { + -ms-flex-preferred-size: calc(58.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(58.33% - 0.01px - 1em); + flex-basis: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(58.33% - 0.01px - 1em); + max-width: calc(58.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-8\@sm { + -ms-flex-preferred-size: calc(66.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(66.66% - 0.01px - 1em); + flex-basis: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(66.66% - 0.01px - 1em); + max-width: calc(66.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-9\@sm { + -ms-flex-preferred-size: calc(75% - 0.01px - 1em); + -ms-flex-preferred-size: calc(75% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(75% - 0.01px - 1em); + flex-basis: calc(75% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(75% - 0.01px - 1em); + max-width: calc(75% - 0.01px - var(--grid-gap, 1em)) + } + .col-10\@sm { + -ms-flex-preferred-size: calc(83.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(83.33% - 0.01px - 1em); + flex-basis: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(83.33% - 0.01px - 1em); + max-width: calc(83.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-11\@sm { + -ms-flex-preferred-size: calc(91.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(91.66% - 0.01px - 1em); + flex-basis: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(91.66% - 0.01px - 1em); + max-width: calc(91.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-12\@sm { + -ms-flex-preferred-size: calc(100% - 0.01px - 1em); + -ms-flex-preferred-size: calc(100% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(100% - 0.01px - 1em); + flex-basis: calc(100% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(100% - 0.01px - 1em); + max-width: calc(100% - 0.01px - var(--grid-gap, 1em)) + } +} + +@media (min-width: 64rem) { + .col\@md { + -ms-flex-positive: 1; + flex-grow: 1; + -ms-flex-preferred-size: 0; + flex-basis: 0; + max-width: 100% + } + .col-1\@md { + -ms-flex-preferred-size: calc(8.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(8.33% - 0.01px - 1em); + flex-basis: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(8.33% - 0.01px - 1em); + max-width: calc(8.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-2\@md { + -ms-flex-preferred-size: calc(16.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(16.66% - 0.01px - 1em); + flex-basis: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(16.66% - 0.01px - 1em); + max-width: calc(16.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-3\@md { + -ms-flex-preferred-size: calc(25% - 0.01px - 1em); + -ms-flex-preferred-size: calc(25% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(25% - 0.01px - 1em); + flex-basis: calc(25% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(25% - 0.01px - 1em); + max-width: calc(25% - 0.01px - var(--grid-gap, 1em)) + } + .col-4\@md { + -ms-flex-preferred-size: calc(33.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(33.33% - 0.01px - 1em); + flex-basis: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(33.33% - 0.01px - 1em); + max-width: calc(33.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-5\@md { + -ms-flex-preferred-size: calc(41.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(41.66% - 0.01px - 1em); + flex-basis: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(41.66% - 0.01px - 1em); + max-width: calc(41.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-6\@md { + -ms-flex-preferred-size: calc(50% - 0.01px - 1em); + -ms-flex-preferred-size: calc(50% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(50% - 0.01px - 1em); + flex-basis: calc(50% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(50% - 0.01px - 1em); + max-width: calc(50% - 0.01px - var(--grid-gap, 1em)) + } + .col-7\@md { + -ms-flex-preferred-size: calc(58.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(58.33% - 0.01px - 1em); + flex-basis: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(58.33% - 0.01px - 1em); + max-width: calc(58.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-8\@md { + -ms-flex-preferred-size: calc(66.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(66.66% - 0.01px - 1em); + flex-basis: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(66.66% - 0.01px - 1em); + max-width: calc(66.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-9\@md { + -ms-flex-preferred-size: calc(75% - 0.01px - 1em); + -ms-flex-preferred-size: calc(75% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(75% - 0.01px - 1em); + flex-basis: calc(75% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(75% - 0.01px - 1em); + max-width: calc(75% - 0.01px - var(--grid-gap, 1em)) + } + .col-10\@md { + -ms-flex-preferred-size: calc(83.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(83.33% - 0.01px - 1em); + flex-basis: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(83.33% - 0.01px - 1em); + max-width: calc(83.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-11\@md { + -ms-flex-preferred-size: calc(91.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(91.66% - 0.01px - 1em); + flex-basis: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(91.66% - 0.01px - 1em); + max-width: calc(91.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-12\@md { + -ms-flex-preferred-size: calc(100% - 0.01px - 1em); + -ms-flex-preferred-size: calc(100% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(100% - 0.01px - 1em); + flex-basis: calc(100% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(100% - 0.01px - 1em); + max-width: calc(100% - 0.01px - var(--grid-gap, 1em)) + } +} + +@media (min-width: 80rem) { + .col\@lg { + -ms-flex-positive: 1; + flex-grow: 1; + -ms-flex-preferred-size: 0; + flex-basis: 0; + max-width: 100% + } + .col-1\@lg { + -ms-flex-preferred-size: calc(8.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(8.33% - 0.01px - 1em); + flex-basis: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(8.33% - 0.01px - 1em); + max-width: calc(8.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-2\@lg { + -ms-flex-preferred-size: calc(16.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(16.66% - 0.01px - 1em); + flex-basis: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(16.66% - 0.01px - 1em); + max-width: calc(16.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-3\@lg { + -ms-flex-preferred-size: calc(25% - 0.01px - 1em); + -ms-flex-preferred-size: calc(25% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(25% - 0.01px - 1em); + flex-basis: calc(25% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(25% - 0.01px - 1em); + max-width: calc(25% - 0.01px - var(--grid-gap, 1em)) + } + .col-4\@lg { + -ms-flex-preferred-size: calc(33.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(33.33% - 0.01px - 1em); + flex-basis: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(33.33% - 0.01px - 1em); + max-width: calc(33.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-5\@lg { + -ms-flex-preferred-size: calc(41.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(41.66% - 0.01px - 1em); + flex-basis: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(41.66% - 0.01px - 1em); + max-width: calc(41.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-6\@lg { + -ms-flex-preferred-size: calc(50% - 0.01px - 1em); + -ms-flex-preferred-size: calc(50% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(50% - 0.01px - 1em); + flex-basis: calc(50% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(50% - 0.01px - 1em); + max-width: calc(50% - 0.01px - var(--grid-gap, 1em)) + } + .col-7\@lg { + -ms-flex-preferred-size: calc(58.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(58.33% - 0.01px - 1em); + flex-basis: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(58.33% - 0.01px - 1em); + max-width: calc(58.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-8\@lg { + -ms-flex-preferred-size: calc(66.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(66.66% - 0.01px - 1em); + flex-basis: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(66.66% - 0.01px - 1em); + max-width: calc(66.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-9\@lg { + -ms-flex-preferred-size: calc(75% - 0.01px - 1em); + -ms-flex-preferred-size: calc(75% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(75% - 0.01px - 1em); + flex-basis: calc(75% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(75% - 0.01px - 1em); + max-width: calc(75% - 0.01px - var(--grid-gap, 1em)) + } + .col-10\@lg { + -ms-flex-preferred-size: calc(83.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(83.33% - 0.01px - 1em); + flex-basis: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(83.33% - 0.01px - 1em); + max-width: calc(83.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-11\@lg { + -ms-flex-preferred-size: calc(91.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(91.66% - 0.01px - 1em); + flex-basis: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(91.66% - 0.01px - 1em); + max-width: calc(91.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-12\@lg { + -ms-flex-preferred-size: calc(100% - 0.01px - 1em); + -ms-flex-preferred-size: calc(100% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(100% - 0.01px - 1em); + flex-basis: calc(100% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(100% - 0.01px - 1em); + max-width: calc(100% - 0.01px - var(--grid-gap, 1em)) + } +} + +@media (min-width: 90rem) { + .col\@xl { + -ms-flex-positive: 1; + flex-grow: 1; + -ms-flex-preferred-size: 0; + flex-basis: 0; + max-width: 100% + } + .col-1\@xl { + -ms-flex-preferred-size: calc(8.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(8.33% - 0.01px - 1em); + flex-basis: calc(8.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(8.33% - 0.01px - 1em); + max-width: calc(8.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-2\@xl { + -ms-flex-preferred-size: calc(16.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(16.66% - 0.01px - 1em); + flex-basis: calc(16.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(16.66% - 0.01px - 1em); + max-width: calc(16.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-3\@xl { + -ms-flex-preferred-size: calc(25% - 0.01px - 1em); + -ms-flex-preferred-size: calc(25% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(25% - 0.01px - 1em); + flex-basis: calc(25% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(25% - 0.01px - 1em); + max-width: calc(25% - 0.01px - var(--grid-gap, 1em)) + } + .col-4\@xl { + -ms-flex-preferred-size: calc(33.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(33.33% - 0.01px - 1em); + flex-basis: calc(33.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(33.33% - 0.01px - 1em); + max-width: calc(33.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-5\@xl { + -ms-flex-preferred-size: calc(41.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(41.66% - 0.01px - 1em); + flex-basis: calc(41.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(41.66% - 0.01px - 1em); + max-width: calc(41.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-6\@xl { + -ms-flex-preferred-size: calc(50% - 0.01px - 1em); + -ms-flex-preferred-size: calc(50% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(50% - 0.01px - 1em); + flex-basis: calc(50% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(50% - 0.01px - 1em); + max-width: calc(50% - 0.01px - var(--grid-gap, 1em)) + } + .col-7\@xl { + -ms-flex-preferred-size: calc(58.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(58.33% - 0.01px - 1em); + flex-basis: calc(58.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(58.33% - 0.01px - 1em); + max-width: calc(58.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-8\@xl { + -ms-flex-preferred-size: calc(66.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(66.66% - 0.01px - 1em); + flex-basis: calc(66.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(66.66% - 0.01px - 1em); + max-width: calc(66.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-9\@xl { + -ms-flex-preferred-size: calc(75% - 0.01px - 1em); + -ms-flex-preferred-size: calc(75% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(75% - 0.01px - 1em); + flex-basis: calc(75% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(75% - 0.01px - 1em); + max-width: calc(75% - 0.01px - var(--grid-gap, 1em)) + } + .col-10\@xl { + -ms-flex-preferred-size: calc(83.33% - 0.01px - 1em); + -ms-flex-preferred-size: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(83.33% - 0.01px - 1em); + flex-basis: calc(83.33% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(83.33% - 0.01px - 1em); + max-width: calc(83.33% - 0.01px - var(--grid-gap, 1em)) + } + .col-11\@xl { + -ms-flex-preferred-size: calc(91.66% - 0.01px - 1em); + -ms-flex-preferred-size: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(91.66% - 0.01px - 1em); + flex-basis: calc(91.66% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(91.66% - 0.01px - 1em); + max-width: calc(91.66% - 0.01px - var(--grid-gap, 1em)) + } + .col-12\@xl { + -ms-flex-preferred-size: calc(100% - 0.01px - 1em); + -ms-flex-preferred-size: calc(100% - 0.01px - var(--grid-gap, 1em)); + flex-basis: calc(100% - 0.01px - 1em); + flex-basis: calc(100% - 0.01px - var(--grid-gap, 1em)); + max-width: calc(100% - 0.01px - 1em); + max-width: calc(100% - 0.01px - var(--grid-gap, 1em)) + } +} + +:root { + --radius-sm: calc(var(--radius, 0.25em)/2); + --radius-md: var(--radius, 0.25em); + --radius-lg: calc(var(--radius, 0.25em)*2); + --shadow-sm: 0 1px 2px rgba(0, 0, 0, .085), 0 1px 8px rgba(0, 0, 0, .1); + --shadow-md: 0 1px 8px rgba(0, 0, 0, .1), 0 8px 24px rgba(0, 0, 0, .15); + --shadow-lg: 0 1px 8px rgba(0, 0, 0, .1), 0 16px 48px rgba(0, 0, 0, .1), 0 24px 60px rgba(0, 0, 0, .1); + --bounce: cubic-bezier(0.175, 0.885, 0.32, 1.275); + --ease-in-out: cubic-bezier(0.645, 0.045, 0.355, 1); + --ease-in: cubic-bezier(0.55, 0.055, 0.675, 0.19); + --ease-out: cubic-bezier(0.215, 0.61, 0.355, 1) +} + +:root { + --body-line-height: 1.4; + --heading-line-height: 1.2 +} + +body { + color: hsl(240, 4%, 20%); + color: var(--color-contrast-high, #313135) +} + +h1, h2, h3, h4 { + color: hsl(240, 8%, 12%); + color: var(--color-contrast-higher, #1c1c21); + line-height: 1.2; + line-height: var(--heading-line-height, 1.2) +} + +.text-xxxl { + font-size: 2.48832em; + font-size: var(--text-xxxl, 2.488em) +} + +small, .text-sm { + font-size: 0.83333em; + font-size: var(--text-sm, 0.833em) +} + +.text-xs { + font-size: 0.69444em; + font-size: var(--text-xs, 0.694em) +} + +strong, .text-bold { + font-weight: bold +} + +s { + text-decoration: line-through +} + +u, .text-underline { + text-decoration: underline +} + + +.text-component h1, .text-component h2, .text-component h3, .text-component h4 { + line-height: 1.2; + line-height: var(--component-heading-line-height, 1.2); + margin-bottom: 0.25em; + margin-bottom: calc(var(--space-xxxs)*var(--text-vspace-multiplier, 1)) +} + +.text-component h2, .text-component h3, .text-component h4 { + margin-top: 0.75em; + margin-top: calc(var(--space-sm)*var(--text-vspace-multiplier, 1)) +} + +.text-component p, .text-component blockquote, .text-component ul li, .text-component ol li { + line-height: 1.4; + line-height: var(--component-body-line-height) +} + +.text-component ul, .text-component ol, .text-component p, .text-component blockquote, .text-component .text-component__block { + margin-bottom: 0.75em; + margin-bottom: calc(var(--space-sm)*var(--text-vspace-multiplier, 1)) +} + +.text-component ul, .text-component ol { + padding-left: 1em +} + +.text-component ul { + list-style-type: disc +} + +.text-component ol { + list-style-type: decimal +} + +.text-component img { + display: block; + margin: 0 auto +} + +.text-component figcaption { + text-align: center; + margin-top: 0.5em; + margin-top: var(--space-xs) +} + +.text-component em { + font-style: italic +} + +.text-component hr { + margin-top: 2em; + margin-top: calc(var(--space-lg)*var(--text-vspace-multiplier, 1)); + margin-bottom: 2em; + margin-bottom: calc(var(--space-lg)*var(--text-vspace-multiplier, 1)); + margin-left: auto; + margin-right: auto +} + +.text-component>*:first-child { + margin-top: 0 +} + +.text-component>*:last-child { + margin-bottom: 0 +} + +.text-component__block--full-width { + width: 100vw; + margin-left: calc(50% - 50vw) +} + +@media (min-width: 48rem) { + .text-component__block--left, .text-component__block--right { + width: 45% + } + .text-component__block--left img, .text-component__block--right img { + width: 100% + } + .text-component__block--left { + float: left; + margin-right: 0.75em; + margin-right: calc(var(--space-sm)*var(--text-vspace-multiplier, 1)) + } + .text-component__block--right { + float: right; + margin-left: 0.75em; + margin-left: calc(var(--space-sm)*var(--text-vspace-multiplier, 1)) + } +} + +@media (min-width: 90rem) { + .text-component__block--outset { + width: calc(100% + 10.5em); + width: calc(100% + 2*var(--space-xxl)) + } + .text-component__block--outset img { + width: 100% + } + .text-component__block--outset:not(.text-component__block--right) { + margin-left: -5.25em; + margin-left: calc(-1*var(--space-xxl)) + } + .text-component__block--left, .text-component__block--right { + width: 50% + } + .text-component__block--right.text-component__block--outset { + margin-right: -5.25em; + margin-right: calc(-1*var(--space-xxl)) + } +} + +:root { + --icon-xxs: 12px; + --icon-xs: 16px; + --icon-sm: 24px; + --icon-md: 32px; + --icon-lg: 48px; + --icon-xl: 64px; + --icon-xxl: 128px +} + +.icon--xxs { + font-size: 12px; + font-size: var(--icon-xxs) +} + +.icon--xs { + font-size: 16px; + font-size: var(--icon-xs) +} + +.icon--sm { + font-size: 24px; + font-size: var(--icon-sm) +} + +.icon--md { + font-size: 32px; + font-size: var(--icon-md) +} + +.icon--lg { + font-size: 48px; + font-size: var(--icon-lg) +} + +.icon--xl { + font-size: 64px; + font-size: var(--icon-xl) +} + +.icon--xxl { + font-size: 128px; + font-size: var(--icon-xxl) +} + +.icon--is-spinning { + -webkit-animation: icon-spin 1s infinite linear; + animation: icon-spin 1s infinite linear +} + +@-webkit-keyframes icon-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg) + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg) + } +} + +@keyframes icon-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg) + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg) + } +} + +.icon use { + color: inherit; + fill: currentColor +} + +.btn { + position: relative; + display: -ms-inline-flexbox; + display: inline-flex; + -ms-flex-pack: center; + justify-content: center; + -ms-flex-align: center; + align-items: center; + white-space: nowrap; + text-decoration: none; + line-height: 1; + font-size: 1em; + font-size: var(--btn-font-size, 1em); + padding-top: 0.5em; + padding-top: var(--btn-padding-y, 0.5em); + padding-bottom: 0.5em; + padding-bottom: var(--btn-padding-y, 0.5em); + padding-left: 0.75em; + padding-left: var(--btn-padding-x, 0.75em); + padding-right: 0.75em; + padding-right: var(--btn-padding-x, 0.75em); + border-radius: 0.25em; + border-radius: var(--btn-radius, 0.25em) +} + +.btn--primary { + background-color: hsl(220, 90%, 56%); + background-color: var(--color-primary, #2a6df4); + color: hsl(0, 0%, 100%); + color: var(--color-white, #fff) +} + +.btn--subtle { + background-color: hsl(240, 1%, 83%); + background-color: var(--color-contrast-low, #d3d3d4); + color: hsl(240, 8%, 12%); + color: var(--color-contrast-higher, #1c1c21) +} + +.btn--accent { + background-color: hsl(355, 90%, 61%); + background-color: var(--color-accent, #f54251); + color: hsl(0, 0%, 100%); + color: var(--color-white, #fff) +} + +.btn--disabled { + cursor: not-allowed +} + +.btn--sm { + font-size: 0.8em; + font-size: var(--btn-font-size-sm, 0.8em) +} + +.btn--md { + font-size: 1.2em; + font-size: var(--btn-font-size-md, 1.2em) +} + +.btn--lg { + font-size: 1.4em; + font-size: var(--btn-font-size-lg, 1.4em) +} + +.btn--icon { + padding: 0.5em; + padding: var(--btn-padding-y, 0.5em) +} + +.form-control { + background-color: hsl(0, 0%, 100%); + background-color: var(--color-bg, #f2f2f2); + padding-top: 0.5em; + padding-top: var(--form-control-padding-y, 0.5em); + padding-bottom: 0.5em; + padding-bottom: var(--form-control-padding-y, 0.5em); + padding-left: 0.75em; + padding-left: var(--form-control-padding-x, 0.75em); + padding-right: 0.75em; + padding-right: var(--form-control-padding-x, 0.75em); + border-radius: 0.25em; + border-radius: var(--form-control-radius, 0.25em) +} + +.form-control::-webkit-input-placeholder { + color: hsl(240, 1%, 48%); + color: var(--color-contrast-medium, #79797c) +} + +.form-control::-moz-placeholder { + opacity: 1; + color: hsl(240, 1%, 48%); + color: var(--color-contrast-medium, #79797c) +} + +.form-control:-ms-input-placeholder { + color: hsl(240, 1%, 48%); + color: var(--color-contrast-medium, #79797c) +} + +.form-control:-moz-placeholder { + color: hsl(240, 1%, 48%); + color: var(--color-contrast-medium, #79797c) +} + +.form-control[disabled], .form-control[readonly] { + cursor: not-allowed +} + +.form-legend { + color: hsl(240, 8%, 12%); + color: var(--color-contrast-higher, #1c1c21); + line-height: 1.2; + font-size: 1.2em; + font-size: var(--text-md, 1.2em); + margin-bottom: 0.375em; + margin-bottom: var(--space-xxs) +} + +.form-label { + display: inline-block +} + +.form__msg-error { + background-color: hsl(355, 90%, 61%); + background-color: var(--color-error, #f54251); + color: hsl(0, 0%, 100%); + color: var(--color-white, #fff); + font-size: 0.83333em; + font-size: var(--text-sm, 0.833em); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + padding: 0.5em; + padding: var(--space-xs); + margin-top: 0.75em; + margin-top: var(--space-sm); + border-radius: 0.25em; + border-radius: var(--radius-md, 0.25em); + position: absolute; + clip: rect(1px, 1px, 1px, 1px) +} + +.form__msg-error::before { + content: ''; + position: absolute; + left: 0.75em; + left: var(--space-sm); + top: 0; + -webkit-transform: translateY(-100%); + -ms-transform: translateY(-100%); + transform: translateY(-100%); + width: 0; + height: 0; + border: 8px solid transparent; + border-bottom-color: hsl(355, 90%, 61%); + border-bottom-color: var(--color-error) +} + +.form__msg-error--is-visible { + position: relative; + clip: auto +} + +.radio-list>*, .checkbox-list>* { + position: relative; + display: -ms-flexbox; + display: flex; + -ms-flex-align: baseline; + align-items: baseline; + margin-bottom: 0.375em; + margin-bottom: var(--space-xxs) +} + +.radio-list>*:last-of-type, .checkbox-list>*:last-of-type { + margin-bottom: 0 +} + +.radio-list label, .checkbox-list label { + line-height: 1.4; + line-height: var(--body-line-height); + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none +} + +.radio-list input, .checkbox-list input { + vertical-align: top; + margin-right: 0.25em; + margin-right: var(--space-xxxs); + -ms-flex-negative: 0; + flex-shrink: 0 +} + +:root { + --zindex-header: 2; + --zindex-popover: 5; + --zindex-fixed-element: 10; + --zindex-overlay: 15 +} + +@media not all and (min-width: 32rem) { + .display\@xs { + display: none !important + } +} + +@media (min-width: 32rem) { + .hide\@xs { + display: none !important + } +} + +@media not all and (min-width: 48rem) { + .display\@sm { + display: none !important + } +} + +@media (min-width: 48rem) { + .hide\@sm { + display: none !important + } +} + +@media not all and (min-width: 64rem) { + .display\@md { + display: none !important + } +} + +@media (min-width: 64rem) { + .hide\@md { + display: none !important + } +} + +@media not all and (min-width: 80rem) { + .display\@lg { + display: none !important + } +} + +@media (min-width: 80rem) { + .hide\@lg { + display: none !important + } +} + +@media not all and (min-width: 90rem) { + .display\@xl { + display: none !important + } +} + +@media (min-width: 90rem) { + .hide\@xl { + display: none !important + } +} + +:root { + --display: block +} + +.is-visible { + display: block !important; + display: var(--display) !important +} + +.is-hidden { + display: none !important +} + +.sr-only { + position: absolute; + clip: rect(1px, 1px, 1px, 1px); + -webkit-clip-path: inset(50%); + clip-path: inset(50%); + width: 1px; + height: 1px; + overflow: hidden; + padding: 0; + border: 0; + white-space: nowrap +} + +.flex { + display: -ms-flexbox; + display: flex +} + +.inline-flex { + display: -ms-inline-flexbox; + display: inline-flex +} + +.flex-wrap { + -ms-flex-wrap: wrap; + flex-wrap: wrap +} + +.flex-column { + -ms-flex-direction: column; + flex-direction: column +} + +.flex-row { + -ms-flex-direction: row; + flex-direction: row +} + +.flex-center { + -ms-flex-pack: center; + justify-content: center; + -ms-flex-align: center; + align-items: center +} + +.justify-start { + -ms-flex-pack: start; + justify-content: flex-start +} + +.justify-end { + -ms-flex-pack: end; + justify-content: flex-end +} + +.justify-center { + -ms-flex-pack: center; + justify-content: center +} + +.justify-between { + -ms-flex-pack: justify; + justify-content: space-between +} + +.items-center { + -ms-flex-align: center; + align-items: center +} + +.items-start { + -ms-flex-align: start; + align-items: flex-start +} + +.items-end { + -ms-flex-align: end; + align-items: flex-end +} + +@media (min-width: 32rem) { + .flex-wrap\@xs { + -ms-flex-wrap: wrap; + flex-wrap: wrap + } + .flex-column\@xs { + -ms-flex-direction: column; + flex-direction: column + } + .flex-row\@xs { + -ms-flex-direction: row; + flex-direction: row + } + .flex-center\@xs { + -ms-flex-pack: center; + justify-content: center; + -ms-flex-align: center; + align-items: center + } + .justify-start\@xs { + -ms-flex-pack: start; + justify-content: flex-start + } + .justify-end\@xs { + -ms-flex-pack: end; + justify-content: flex-end + } + .justify-center\@xs { + -ms-flex-pack: center; + justify-content: center + } + .justify-between\@xs { + -ms-flex-pack: justify; + justify-content: space-between + } + .items-center\@xs { + -ms-flex-align: center; + align-items: center + } + .items-start\@xs { + -ms-flex-align: start; + align-items: flex-start + } + .items-end\@xs { + -ms-flex-align: end; + align-items: flex-end + } +} + +@media (min-width: 48rem) { + .flex-wrap\@sm { + -ms-flex-wrap: wrap; + flex-wrap: wrap + } + .flex-column\@sm { + -ms-flex-direction: column; + flex-direction: column + } + .flex-row\@sm { + -ms-flex-direction: row; + flex-direction: row + } + .flex-center\@sm { + -ms-flex-pack: center; + justify-content: center; + -ms-flex-align: center; + align-items: center + } + .justify-start\@sm { + -ms-flex-pack: start; + justify-content: flex-start + } + .justify-end\@sm { + -ms-flex-pack: end; + justify-content: flex-end + } + .justify-center\@sm { + -ms-flex-pack: center; + justify-content: center + } + .justify-between\@sm { + -ms-flex-pack: justify; + justify-content: space-between + } + .items-center\@sm { + -ms-flex-align: center; + align-items: center + } + .items-start\@sm { + -ms-flex-align: start; + align-items: flex-start + } + .items-end\@sm { + -ms-flex-align: end; + align-items: flex-end + } +} + +@media (min-width: 64rem) { + .flex-wrap\@md { + -ms-flex-wrap: wrap; + flex-wrap: wrap + } + .flex-column\@md { + -ms-flex-direction: column; + flex-direction: column + } + .flex-row\@md { + -ms-flex-direction: row; + flex-direction: row + } + .flex-center\@md { + -ms-flex-pack: center; + justify-content: center; + -ms-flex-align: center; + align-items: center + } + .justify-start\@md { + -ms-flex-pack: start; + justify-content: flex-start + } + .justify-end\@md { + -ms-flex-pack: end; + justify-content: flex-end + } + .justify-center\@md { + -ms-flex-pack: center; + justify-content: center + } + .justify-between\@md { + -ms-flex-pack: justify; + justify-content: space-between + } + .items-center\@md { + -ms-flex-align: center; + align-items: center + } + .items-start\@md { + -ms-flex-align: start; + align-items: flex-start + } + .items-end\@md { + -ms-flex-align: end; + align-items: flex-end + } +} + +@media (min-width: 80rem) { + .flex-wrap\@lg { + -ms-flex-wrap: wrap; + flex-wrap: wrap + } + .flex-column\@lg { + -ms-flex-direction: column; + flex-direction: column + } + .flex-row\@lg { + -ms-flex-direction: row; + flex-direction: row + } + .flex-center\@lg { + -ms-flex-pack: center; + justify-content: center; + -ms-flex-align: center; + align-items: center + } + .justify-start\@lg { + -ms-flex-pack: start; + justify-content: flex-start + } + .justify-end\@lg { + -ms-flex-pack: end; + justify-content: flex-end + } + .justify-center\@lg { + -ms-flex-pack: center; + justify-content: center + } + .justify-between\@lg { + -ms-flex-pack: justify; + justify-content: space-between + } + .items-center\@lg { + -ms-flex-align: center; + align-items: center + } + .items-start\@lg { + -ms-flex-align: start; + align-items: flex-start + } + .items-end\@lg { + -ms-flex-align: end; + align-items: flex-end + } +} + +@media (min-width: 90rem) { + .flex-wrap\@xl { + -ms-flex-wrap: wrap; + flex-wrap: wrap + } + .flex-column\@xl { + -ms-flex-direction: column; + flex-direction: column + } + .flex-row\@xl { + -ms-flex-direction: row; + flex-direction: row + } + .flex-center\@xl { + -ms-flex-pack: center; + justify-content: center; + -ms-flex-align: center; + align-items: center + } + .justify-start\@xl { + -ms-flex-pack: start; + justify-content: flex-start + } + .justify-end\@xl { + -ms-flex-pack: end; + justify-content: flex-end + } + .justify-center\@xl { + -ms-flex-pack: center; + justify-content: center + } + .justify-between\@xl { + -ms-flex-pack: justify; + justify-content: space-between + } + .items-center\@xl { + -ms-flex-align: center; + align-items: center + } + .items-start\@xl { + -ms-flex-align: start; + align-items: flex-start + } + .items-end\@xl { + -ms-flex-align: end; + align-items: flex-end + } +} + +.flex-grow { + -ms-flex-positive: 1; + flex-grow: 1 +} + +.flex-shrink-0 { + -ms-flex-negative: 0; + flex-shrink: 0 +} + +.flex-gap-xxxs { + margin-bottom: -0.25em; + margin-bottom: calc(-1*var(--space-xxxs)); + margin-right: -0.25em; + margin-right: calc(-1*var(--space-xxxs)) +} + +.flex-gap-xxxs>* { + margin-bottom: 0.25em; + margin-bottom: var(--space-xxxs); + margin-right: 0.25em; + margin-right: var(--space-xxxs) +} + +.flex-gap-xxs { + margin-bottom: -0.375em; + margin-bottom: calc(-1*var(--space-xxs)); + margin-right: -0.375em; + margin-right: calc(-1*var(--space-xxs)) +} + +.flex-gap-xxs>* { + margin-bottom: 0.375em; + margin-bottom: var(--space-xxs); + margin-right: 0.375em; + margin-right: var(--space-xxs) +} + +.flex-gap-xs { + margin-bottom: -0.5em; + margin-bottom: calc(-1*var(--space-xs)); + margin-right: -0.5em; + margin-right: calc(-1*var(--space-xs)) +} + +.flex-gap-xs>* { + margin-bottom: 0.5em; + margin-bottom: var(--space-xs); + margin-right: 0.5em; + margin-right: var(--space-xs) +} + +.flex-gap-sm { + margin-bottom: -0.75em; + margin-bottom: calc(-1*var(--space-sm)); + margin-right: -0.75em; + margin-right: calc(-1*var(--space-sm)) +} + +.flex-gap-sm>* { + margin-bottom: 0.75em; + margin-bottom: var(--space-sm); + margin-right: 0.75em; + margin-right: var(--space-sm) +} + +.flex-gap-md { + margin-bottom: -1.25em; + margin-bottom: calc(-1*var(--space-md)); + margin-right: -1.25em; + margin-right: calc(-1*var(--space-md)) +} + +.flex-gap-md>* { + margin-bottom: 1.25em; + margin-bottom: var(--space-md); + margin-right: 1.25em; + margin-right: var(--space-md) +} + +.flex-gap-lg { + margin-bottom: -2em; + margin-bottom: calc(-1*var(--space-lg)); + margin-right: -2em; + margin-right: calc(-1*var(--space-lg)) +} + +.flex-gap-lg>* { + margin-bottom: 2em; + margin-bottom: var(--space-lg); + margin-right: 2em; + margin-right: var(--space-lg) +} + +.flex-gap-xl { + margin-bottom: -3.25em; + margin-bottom: calc(-1*var(--space-xl)); + margin-right: -3.25em; + margin-right: calc(-1*var(--space-xl)) +} + +.flex-gap-xl>* { + margin-bottom: 3.25em; + margin-bottom: var(--space-xl); + margin-right: 3.25em; + margin-right: var(--space-xl) +} + +.flex-gap-xxl { + margin-bottom: -5.25em; + margin-bottom: calc(-1*var(--space-xxl)); + margin-right: -5.25em; + margin-right: calc(-1*var(--space-xxl)) +} + +.flex-gap-xxl>* { + margin-bottom: 5.25em; + margin-bottom: var(--space-xxl); + margin-right: 5.25em; + margin-right: var(--space-xxl) +} + +.margin-xxxxs { + margin: 0.125em; + margin: var(--space-xxxxs) +} + +.margin-xxxs { + margin: 0.25em; + margin: var(--space-xxxs) +} + +.margin-xxs { + margin: 0.375em; + margin: var(--space-xxs) +} + +.margin-xs { + margin: 0.5em; + margin: var(--space-xs) +} + +.margin-sm { + margin: 0.75em; + margin: var(--space-sm) +} + +.margin-md { + margin: 1.25em; + margin: var(--space-md) +} + +.margin-lg { + margin: 2em; + margin: var(--space-lg) +} + +.margin-xl { + margin: 3.25em; + margin: var(--space-xl) +} + +.margin-xxl { + margin: 5.25em; + margin: var(--space-xxl) +} + +.margin-xxxl { + margin: 8.5em; + margin: var(--space-xxxl) +} + +.margin-xxxxl { + margin: 13.75em; + margin: var(--space-xxxxl) +} + +.margin-auto { + margin: auto +} + +.margin-top-xxxxs { + margin-top: 0.125em; + margin-top: var(--space-xxxxs) +} + +.margin-top-xxxs { + margin-top: 0.25em; + margin-top: var(--space-xxxs) +} + +.margin-top-xxs { + margin-top: 0.375em; + margin-top: var(--space-xxs) +} + +.margin-top-xs { + margin-top: 0.5em; + margin-top: var(--space-xs) +} + +.margin-top-sm { + margin-top: 0.75em; + margin-top: var(--space-sm) +} + +.margin-top-md { + margin-top: 1.25em; + margin-top: var(--space-md) +} + +.margin-top-lg { + margin-top: 2em; + margin-top: var(--space-lg) +} + +.margin-top-xl { + margin-top: 3.25em; + margin-top: var(--space-xl) +} + +.margin-top-xxl { + margin-top: 5.25em; + margin-top: var(--space-xxl) +} + +.margin-top-xxxl { + margin-top: 8.5em; + margin-top: var(--space-xxxl) +} + +.margin-top-xxxxl { + margin-top: 13.75em; + margin-top: var(--space-xxxxl) +} + +.margin-top-auto { + margin-top: auto +} + +.margin-bottom-xxxxs { + margin-bottom: 0.125em; + margin-bottom: var(--space-xxxxs) +} + +.margin-bottom-xxxs { + margin-bottom: 0.25em; + margin-bottom: var(--space-xxxs) +} + +.margin-bottom-xxs { + margin-bottom: 0.375em; + margin-bottom: var(--space-xxs) +} + +.margin-bottom-xs { + margin-bottom: 0.5em; + margin-bottom: var(--space-xs) +} + +.margin-bottom-sm { + margin-bottom: 0.75em; + margin-bottom: var(--space-sm) +} + +.margin-bottom-md { + margin-bottom: 1.25em; + margin-bottom: var(--space-md) +} + +.margin-bottom-lg { + margin-bottom: 2em; + margin-bottom: var(--space-lg) +} + +.margin-bottom-xl { + margin-bottom: 3.25em; + margin-bottom: var(--space-xl) +} + +.margin-bottom-xxl { + margin-bottom: 5.25em; + margin-bottom: var(--space-xxl) +} + +.margin-bottom-xxxl { + margin-bottom: 8.5em; + margin-bottom: var(--space-xxxl) +} + +.margin-bottom-xxxxl { + margin-bottom: 13.75em; + margin-bottom: var(--space-xxxxl) +} + +.margin-bottom-auto { + margin-bottom: auto +} + +.margin-right-xxxxs { + margin-right: 0.125em; + margin-right: var(--space-xxxxs) +} + +.margin-right-xxxs { + margin-right: 0.25em; + margin-right: var(--space-xxxs) +} + +.margin-right-xxs { + margin-right: 0.375em; + margin-right: var(--space-xxs) +} + +.margin-right-xs { + margin-right: 0.5em; + margin-right: var(--space-xs) +} + +.margin-right-sm { + margin-right: 0.75em; + margin-right: var(--space-sm) +} + +.margin-right-md { + margin-right: 1.25em; + margin-right: var(--space-md) +} + +.margin-right-lg { + margin-right: 2em; + margin-right: var(--space-lg) +} + +.margin-right-xl { + margin-right: 3.25em; + margin-right: var(--space-xl) +} + +.margin-right-xxl { + margin-right: 5.25em; + margin-right: var(--space-xxl) +} + +.margin-right-xxxl { + margin-right: 8.5em; + margin-right: var(--space-xxxl) +} + +.margin-right-xxxxl { + margin-right: 13.75em; + margin-right: var(--space-xxxxl) +} + +.margin-right-auto { + margin-right: auto +} + +.margin-left-xxxxs { + margin-left: 0.125em; + margin-left: var(--space-xxxxs) +} + +.margin-left-xxxs { + margin-left: 0.25em; + margin-left: var(--space-xxxs) +} + +.margin-left-xxs { + margin-left: 0.375em; + margin-left: var(--space-xxs) +} + +.margin-left-xs { + margin-left: 0.5em; + margin-left: var(--space-xs) +} + +.margin-left-sm { + margin-left: 0.75em; + margin-left: var(--space-sm) +} + +.margin-left-md { + margin-left: 1.25em; + margin-left: var(--space-md) +} + +.margin-left-lg { + margin-left: 2em; + margin-left: var(--space-lg) +} + +.margin-left-xl { + margin-left: 3.25em; + margin-left: var(--space-xl) +} + +.margin-left-xxl { + margin-left: 5.25em; + margin-left: var(--space-xxl) +} + +.margin-left-xxxl { + margin-left: 8.5em; + margin-left: var(--space-xxxl) +} + +.margin-left-xxxxl { + margin-left: 13.75em; + margin-left: var(--space-xxxxl) +} + +.margin-left-auto { + margin-left: auto +} + +.margin-x-xxxxs { + margin-left: 0.125em; + margin-left: var(--space-xxxxs); + margin-right: 0.125em; + margin-right: var(--space-xxxxs) +} + +.margin-x-xxxs { + margin-left: 0.25em; + margin-left: var(--space-xxxs); + margin-right: 0.25em; + margin-right: var(--space-xxxs) +} + +.margin-x-xxs { + margin-left: 0.375em; + margin-left: var(--space-xxs); + margin-right: 0.375em; + margin-right: var(--space-xxs) +} + +.margin-x-xs { + margin-left: 0.5em; + margin-left: var(--space-xs); + margin-right: 0.5em; + margin-right: var(--space-xs) +} + +.margin-x-sm { + margin-left: 0.75em; + margin-left: var(--space-sm); + margin-right: 0.75em; + margin-right: var(--space-sm) +} + +.margin-x-md { + margin-left: 1.25em; + margin-left: var(--space-md); + margin-right: 1.25em; + margin-right: var(--space-md) +} + +.margin-x-lg { + margin-left: 2em; + margin-left: var(--space-lg); + margin-right: 2em; + margin-right: var(--space-lg) +} + +.margin-x-xl { + margin-left: 3.25em; + margin-left: var(--space-xl); + margin-right: 3.25em; + margin-right: var(--space-xl) +} + +.margin-x-xxl { + margin-left: 5.25em; + margin-left: var(--space-xxl); + margin-right: 5.25em; + margin-right: var(--space-xxl) +} + +.margin-x-xxxl { + margin-left: 8.5em; + margin-left: var(--space-xxxl); + margin-right: 8.5em; + margin-right: var(--space-xxxl) +} + +.margin-x-xxxxl { + margin-left: 13.75em; + margin-left: var(--space-xxxxl); + margin-right: 13.75em; + margin-right: var(--space-xxxxl) +} + +.margin-x-auto { + margin-left: auto; + margin-right: auto +} + +.margin-y-xxxxs { + margin-top: 0.125em; + margin-top: var(--space-xxxxs); + margin-bottom: 0.125em; + margin-bottom: var(--space-xxxxs) +} + +.margin-y-xxxs { + margin-top: 0.25em; + margin-top: var(--space-xxxs); + margin-bottom: 0.25em; + margin-bottom: var(--space-xxxs) +} + +.margin-y-xxs { + margin-top: 0.375em; + margin-top: var(--space-xxs); + margin-bottom: 0.375em; + margin-bottom: var(--space-xxs) +} + +.margin-y-xs { + margin-top: 0.5em; + margin-top: var(--space-xs); + margin-bottom: 0.5em; + margin-bottom: var(--space-xs) +} + +.margin-y-sm { + margin-top: 0.75em; + margin-top: var(--space-sm); + margin-bottom: 0.75em; + margin-bottom: var(--space-sm) +} + +.margin-y-md { + margin-top: 1.25em; + margin-top: var(--space-md); + margin-bottom: 1.25em; + margin-bottom: var(--space-md) +} + +.margin-y-lg { + margin-top: 2em; + margin-top: var(--space-lg); + margin-bottom: 2em; + margin-bottom: var(--space-lg) +} + +.margin-y-xl { + margin-top: 3.25em; + margin-top: var(--space-xl); + margin-bottom: 3.25em; + margin-bottom: var(--space-xl) +} + +.margin-y-xxl { + margin-top: 5.25em; + margin-top: var(--space-xxl); + margin-bottom: 5.25em; + margin-bottom: var(--space-xxl) +} + +.margin-y-xxxl { + margin-top: 8.5em; + margin-top: var(--space-xxxl); + margin-bottom: 8.5em; + margin-bottom: var(--space-xxxl) +} + +.margin-y-xxxxl { + margin-top: 13.75em; + margin-top: var(--space-xxxxl); + margin-bottom: 13.75em; + margin-bottom: var(--space-xxxxl) +} + +.margin-y-auto { + margin-top: auto; + margin-bottom: auto +} + +@media not all and (min-width: 32rem) { + .has-margin\@xs { + margin: 0 !important + } +} + +@media not all and (min-width: 48rem) { + .has-margin\@sm { + margin: 0 !important + } +} + +@media not all and (min-width: 64rem) { + .has-margin\@md { + margin: 0 !important + } +} + +@media not all and (min-width: 80rem) { + .has-margin\@lg { + margin: 0 !important + } +} + +@media not all and (min-width: 90rem) { + .has-margin\@xl { + margin: 0 !important + } +} + +.padding-md { + padding: 1.25em; + padding: var(--space-md) +} + +.padding-xxxxs { + padding: 0.125em; + padding: var(--space-xxxxs) +} + +.padding-xxxs { + padding: 0.25em; + padding: var(--space-xxxs) +} + +.padding-xxs { + padding: 0.375em; + padding: var(--space-xxs) +} + +.padding-xs { + padding: 0.5em; + padding: var(--space-xs) +} + +.padding-sm { + padding: 0.75em; + padding: var(--space-sm) +} + +.padding-lg { + padding: 2em; + padding: var(--space-lg) +} + +.padding-xl { + padding: 3.25em; + padding: var(--space-xl) +} + +.padding-xxl { + padding: 5.25em; + padding: var(--space-xxl) +} + +.padding-xxxl { + padding: 8.5em; + padding: var(--space-xxxl) +} + +.padding-xxxxl { + padding: 13.75em; + padding: var(--space-xxxxl) +} + +.padding-component { + padding: 1.25em; + padding: var(--component-padding) +} + +.padding-top-md { + padding-top: 1.25em; + padding-top: var(--space-md) +} + +.padding-top-xxxxs { + padding-top: 0.125em; + padding-top: var(--space-xxxxs) +} + +.padding-top-xxxs { + padding-top: 0.25em; + padding-top: var(--space-xxxs) +} + +.padding-top-xxs { + padding-top: 0.375em; + padding-top: var(--space-xxs) +} + +.padding-top-xs { + padding-top: 0.5em; + padding-top: var(--space-xs) +} + +.padding-top-sm { + padding-top: 0.75em; + padding-top: var(--space-sm) +} + +.padding-top-lg { + padding-top: 2em; + padding-top: var(--space-lg) +} + +.padding-top-xl { + padding-top: 3.25em; + padding-top: var(--space-xl) +} + +.padding-top-xxl { + padding-top: 5.25em; + padding-top: var(--space-xxl) +} + +.padding-top-xxxl { + padding-top: 8.5em; + padding-top: var(--space-xxxl) +} + +.padding-top-xxxxl { + padding-top: 13.75em; + padding-top: var(--space-xxxxl) +} + +.padding-top-component { + padding-top: 1.25em; + padding-top: var(--component-padding) +} + +.padding-bottom-md { + padding-bottom: 1.25em; + padding-bottom: var(--space-md) +} + +.padding-bottom-xxxxs { + padding-bottom: 0.125em; + padding-bottom: var(--space-xxxxs) +} + +.padding-bottom-xxxs { + padding-bottom: 0.25em; + padding-bottom: var(--space-xxxs) +} + +.padding-bottom-xxs { + padding-bottom: 0.375em; + padding-bottom: var(--space-xxs) +} + +.padding-bottom-xs { + padding-bottom: 0.5em; + padding-bottom: var(--space-xs) +} + +.padding-bottom-sm { + padding-bottom: 0.75em; + padding-bottom: var(--space-sm) +} + +.padding-bottom-lg { + padding-bottom: 2em; + padding-bottom: var(--space-lg) +} + +.padding-bottom-xl { + padding-bottom: 3.25em; + padding-bottom: var(--space-xl) +} + +.padding-bottom-xxl { + padding-bottom: 5.25em; + padding-bottom: var(--space-xxl) +} + +.padding-bottom-xxxl { + padding-bottom: 8.5em; + padding-bottom: var(--space-xxxl) +} + +.padding-bottom-xxxxl { + padding-bottom: 13.75em; + padding-bottom: var(--space-xxxxl) +} + +.padding-bottom-component { + padding-bottom: 1.25em; + padding-bottom: var(--component-padding) +} + +.padding-right-md { + padding-right: 1.25em; + padding-right: var(--space-md) +} + +.padding-right-xxxxs { + padding-right: 0.125em; + padding-right: var(--space-xxxxs) +} + +.padding-right-xxxs { + padding-right: 0.25em; + padding-right: var(--space-xxxs) +} + +.padding-right-xxs { + padding-right: 0.375em; + padding-right: var(--space-xxs) +} + +.padding-right-xs { + padding-right: 0.5em; + padding-right: var(--space-xs) +} + +.padding-right-sm { + padding-right: 0.75em; + padding-right: var(--space-sm) +} + +.padding-right-lg { + padding-right: 2em; + padding-right: var(--space-lg) +} + +.padding-right-xl { + padding-right: 3.25em; + padding-right: var(--space-xl) +} + +.padding-right-xxl { + padding-right: 5.25em; + padding-right: var(--space-xxl) +} + +.padding-right-xxxl { + padding-right: 8.5em; + padding-right: var(--space-xxxl) +} + +.padding-right-xxxxl { + padding-right: 13.75em; + padding-right: var(--space-xxxxl) +} + +.padding-right-component { + padding-right: 1.25em; + padding-right: var(--component-padding) +} + +.padding-left-md { + padding-left: 1.25em; + padding-left: var(--space-md) +} + +.padding-left-xxxxs { + padding-left: 0.125em; + padding-left: var(--space-xxxxs) +} + +.padding-left-xxxs { + padding-left: 0.25em; + padding-left: var(--space-xxxs) +} + +.padding-left-xxs { + padding-left: 0.375em; + padding-left: var(--space-xxs) +} + +.padding-left-xs { + padding-left: 0.5em; + padding-left: var(--space-xs) +} + +.padding-left-sm { + padding-left: 0.75em; + padding-left: var(--space-sm) +} + +.padding-left-lg { + padding-left: 2em; + padding-left: var(--space-lg) +} + +.padding-left-xl { + padding-left: 3.25em; + padding-left: var(--space-xl) +} + +.padding-left-xxl { + padding-left: 5.25em; + padding-left: var(--space-xxl) +} + +.padding-left-xxxl { + padding-left: 8.5em; + padding-left: var(--space-xxxl) +} + +.padding-left-xxxxl { + padding-left: 13.75em; + padding-left: var(--space-xxxxl) +} + +.padding-left-component { + padding-left: 1.25em; + padding-left: var(--component-padding) +} + +.padding-x-md { + padding-left: 1.25em; + padding-left: var(--space-md); + padding-right: 1.25em; + padding-right: var(--space-md) +} + +.padding-x-xxxxs { + padding-left: 0.125em; + padding-left: var(--space-xxxxs); + padding-right: 0.125em; + padding-right: var(--space-xxxxs) +} + +.padding-x-xxxs { + padding-left: 0.25em; + padding-left: var(--space-xxxs); + padding-right: 0.25em; + padding-right: var(--space-xxxs) +} + +.padding-x-xxs { + padding-left: 0.375em; + padding-left: var(--space-xxs); + padding-right: 0.375em; + padding-right: var(--space-xxs) +} + +.padding-x-xs { + padding-left: 0.5em; + padding-left: var(--space-xs); + padding-right: 0.5em; + padding-right: var(--space-xs) +} + +.padding-x-sm { + padding-left: 0.75em; + padding-left: var(--space-sm); + padding-right: 0.75em; + padding-right: var(--space-sm) +} + +.padding-x-lg { + padding-left: 2em; + padding-left: var(--space-lg); + padding-right: 2em; + padding-right: var(--space-lg) +} + +.padding-x-xl { + padding-left: 3.25em; + padding-left: var(--space-xl); + padding-right: 3.25em; + padding-right: var(--space-xl) +} + +.padding-x-xxl { + padding-left: 5.25em; + padding-left: var(--space-xxl); + padding-right: 5.25em; + padding-right: var(--space-xxl) +} + +.padding-x-xxxl { + padding-left: 8.5em; + padding-left: var(--space-xxxl); + padding-right: 8.5em; + padding-right: var(--space-xxxl) +} + +.padding-x-xxxxl { + padding-left: 13.75em; + padding-left: var(--space-xxxxl); + padding-right: 13.75em; + padding-right: var(--space-xxxxl) +} + +.padding-x-component { + padding-left: 1.25em; + padding-left: var(--component-padding); + padding-right: 1.25em; + padding-right: var(--component-padding) +} + +.padding-y-md { + padding-top: 1.25em; + padding-top: var(--space-md); + padding-bottom: 1.25em; + padding-bottom: var(--space-md) +} + +.padding-y-xxxxs { + padding-top: 0.125em; + padding-top: var(--space-xxxxs); + padding-bottom: 0.125em; + padding-bottom: var(--space-xxxxs) +} + +.padding-y-xxxs { + padding-top: 0.25em; + padding-top: var(--space-xxxs); + padding-bottom: 0.25em; + padding-bottom: var(--space-xxxs) +} + +.padding-y-xxs { + padding-top: 0.375em; + padding-top: var(--space-xxs); + padding-bottom: 0.375em; + padding-bottom: var(--space-xxs) +} + +.padding-y-xs { + padding-top: 0.5em; + padding-top: var(--space-xs); + padding-bottom: 0.5em; + padding-bottom: var(--space-xs) +} + +.padding-y-sm { + padding-top: 0.75em; + padding-top: var(--space-sm); + padding-bottom: 0.75em; + padding-bottom: var(--space-sm) +} + +.padding-y-lg { + padding-top: 2em; + padding-top: var(--space-lg); + padding-bottom: 2em; + padding-bottom: var(--space-lg) +} + +.padding-y-xl { + padding-top: 3.25em; + padding-top: var(--space-xl); + padding-bottom: 3.25em; + padding-bottom: var(--space-xl) +} + +.padding-y-xxl { + padding-top: 5.25em; + padding-top: var(--space-xxl); + padding-bottom: 5.25em; + padding-bottom: var(--space-xxl) +} + +.padding-y-xxxl { + padding-top: 8.5em; + padding-top: var(--space-xxxl); + padding-bottom: 8.5em; + padding-bottom: var(--space-xxxl) +} + +.padding-y-xxxxl { + padding-top: 13.75em; + padding-top: var(--space-xxxxl); + padding-bottom: 13.75em; + padding-bottom: var(--space-xxxxl) +} + +.padding-y-component { + padding-top: 1.25em; + padding-top: var(--component-padding); + padding-bottom: 1.25em; + padding-bottom: var(--component-padding) +} + +@media not all and (min-width: 32rem) { + .has-padding\@xs { + padding: 0 !important + } +} + +@media not all and (min-width: 48rem) { + .has-padding\@sm { + padding: 0 !important + } +} + +@media not all and (min-width: 64rem) { + .has-padding\@md { + padding: 0 !important + } +} + +@media not all and (min-width: 80rem) { + .has-padding\@lg { + padding: 0 !important + } +} + +@media not all and (min-width: 90rem) { + .has-padding\@xl { + padding: 0 !important + } +} + +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap +} + +.text-replace { + overflow: hidden; + color: transparent; + text-indent: 100%; + white-space: nowrap +} + +.text-center { + text-align: center +} + +.text-left { + text-align: left +} + +.text-right { + text-align: right +} + +@media (min-width: 32rem) { + .text-center\@xs { + text-align: center + } + .text-left\@xs { + text-align: left + } + .text-right\@xs { + text-align: right + } +} + +@media (min-width: 48rem) { + .text-center\@sm { + text-align: center + } + .text-left\@sm { + text-align: left + } + .text-right\@sm { + text-align: right + } +} + +@media (min-width: 64rem) { + .text-center\@md { + text-align: center + } + .text-left\@md { + text-align: left + } + .text-right\@md { + text-align: right + } +} + +@media (min-width: 80rem) { + .text-center\@lg { + text-align: center + } + .text-left\@lg { + text-align: left + } + .text-right\@lg { + text-align: right + } +} + +@media (min-width: 90rem) { + .text-center\@xl { + text-align: center + } + .text-left\@xl { + text-align: left + } + .text-right\@xl { + text-align: right + } +} + +.color-inherit { + color: inherit +} + +.color-contrast-medium { + color: hsl(240, 1%, 48%); + color: var(--color-contrast-medium, #79797c) +} + +.color-contrast-high { + color: hsl(240, 4%, 20%); + color: var(--color-contrast-high, #313135) +} + +.color-contrast-higher { + color: hsl(240, 8%, 12%); + color: var(--color-contrast-higher, #1c1c21) +} + +.color-primary { + color: hsl(220, 90%, 56%); + color: var(--color-primary, #2a6df4) +} + +.color-accent { + color: hsl(355, 90%, 61%); + color: var(--color-accent, #f54251) +} + +.color-success { + color: hsl(94, 48%, 56%); + color: var(--color-success, #88c559) +} + +.color-warning { + color: hsl(46, 100%, 61%); + color: var(--color-warning, #ffd138) +} + +.color-error { + color: hsl(355, 90%, 61%); + color: var(--color-error, #f54251) +} + +.width-100\% { + width: 100% +} + +.height-100\% { + height: 100% +} + +.media-wrapper { + position: relative; + height: 0; + padding-bottom: 56.25% +} + +.media-wrapper iframe, .media-wrapper video, .media-wrapper img { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100% +} + +.media-wrapper video, .media-wrapper img { + -o-object-fit: cover; + object-fit: cover +} + +.media-wrapper--4\:3 { + padding-bottom: 75% +} + +:root, [data-theme="default"] { + --color-primary-darker: hsl(220, 90%, 36%); + --color-primary-darker-h: 220; + --color-primary-darker-s: 90%; + --color-primary-darker-l: 36%; + --color-primary-dark: hsl(220, 90%, 46%); + --color-primary-dark-h: 220; + --color-primary-dark-s: 90%; + --color-primary-dark-l: 46%; + --color-primary: hsl(220, 90%, 56%); + --color-primary-h: 220; + --color-primary-s: 90%; + --color-primary-l: 56%; + --color-primary-light: hsl(220, 90%, 66%); + --color-primary-light-h: 220; + --color-primary-light-s: 90%; + --color-primary-light-l: 66%; + --color-primary-lighter: hsl(220, 90%, 76%); + --color-primary-lighter-h: 220; + --color-primary-lighter-s: 90%; + --color-primary-lighter-l: 76%; + --color-accent-darker: hsl(355, 90%, 41%); + --color-accent-darker-h: 355; + --color-accent-darker-s: 90%; + --color-accent-darker-l: 41%; + --color-accent-dark: hsl(355, 90%, 51%); + --color-accent-dark-h: 355; + --color-accent-dark-s: 90%; + --color-accent-dark-l: 51%; + --color-accent: hsl(355, 90%, 61%); + --color-accent-h: 355; + --color-accent-s: 90%; + --color-accent-l: 61%; + --color-accent-light: hsl(355, 90%, 71%); + --color-accent-light-h: 355; + --color-accent-light-s: 90%; + --color-accent-light-l: 71%; + --color-accent-lighter: hsl(355, 90%, 81%); + --color-accent-lighter-h: 355; + --color-accent-lighter-s: 90%; + --color-accent-lighter-l: 81%; + --color-black: hsl(240, 8%, 12%); + --color-black-h: 240; + --color-black-s: 8%; + --color-black-l: 12%; + --color-white: hsl(0, 0%, 100%); + --color-white-h: 0; + --color-white-s: 0%; + --color-white-l: 100%; + --color-success-darker: hsl(94, 48%, 36%); + --color-success-darker-h: 94; + --color-success-darker-s: 48%; + --color-success-darker-l: 36%; + --color-success-dark: hsl(94, 48%, 46%); + --color-success-dark-h: 94; + --color-success-dark-s: 48%; + --color-success-dark-l: 46%; + --color-success: hsl(94, 48%, 56%); + --color-success-h: 94; + --color-success-s: 48%; + --color-success-l: 56%; + --color-success-light: hsl(94, 48%, 66%); + --color-success-light-h: 94; + --color-success-light-s: 48%; + --color-success-light-l: 66%; + --color-success-lighter: hsl(94, 48%, 76%); + --color-success-lighter-h: 94; + --color-success-lighter-s: 48%; + --color-success-lighter-l: 76%; + --color-error-darker: hsl(355, 90%, 41%); + --color-error-darker-h: 355; + --color-error-darker-s: 90%; + --color-error-darker-l: 41%; + --color-error-dark: hsl(355, 90%, 51%); + --color-error-dark-h: 355; + --color-error-dark-s: 90%; + --color-error-dark-l: 51%; + --color-error: hsl(355, 90%, 61%); + --color-error-h: 355; + --color-error-s: 90%; + --color-error-l: 61%; + --color-error-light: hsl(355, 90%, 71%); + --color-error-light-h: 355; + --color-error-light-s: 90%; + --color-error-light-l: 71%; + --color-error-lighter: hsl(355, 90%, 81%); + --color-error-lighter-h: 355; + --color-error-lighter-s: 90%; + --color-error-lighter-l: 81%; + --color-warning-darker: hsl(46, 100%, 41%); + --color-warning-darker-h: 46; + --color-warning-darker-s: 100%; + --color-warning-darker-l: 41%; + --color-warning-dark: hsl(46, 100%, 51%); + --color-warning-dark-h: 46; + --color-warning-dark-s: 100%; + --color-warning-dark-l: 51%; + --color-warning: hsl(46, 100%, 61%); + --color-warning-h: 46; + --color-warning-s: 100%; + --color-warning-l: 61%; + --color-warning-light: hsl(46, 100%, 71%); + --color-warning-light-h: 46; + --color-warning-light-s: 100%; + --color-warning-light-l: 71%; + --color-warning-lighter: hsl(46, 100%, 81%); + --color-warning-lighter-h: 46; + --color-warning-lighter-s: 100%; + --color-warning-lighter-l: 81%; + --color-bg: hsl(0, 0%, 100%); + --color-bg-h: 0; + --color-bg-s: 0%; + --color-bg-l: 100%; + --color-contrast-lower: hsl(0, 0%, 95%); + --color-contrast-lower-h: 0; + --color-contrast-lower-s: 0%; + --color-contrast-lower-l: 95%; + --color-contrast-low: hsl(240, 1%, 83%); + --color-contrast-low-h: 240; + --color-contrast-low-s: 1%; + --color-contrast-low-l: 83%; + --color-contrast-medium: hsl(240, 1%, 48%); + --color-contrast-medium-h: 240; + --color-contrast-medium-s: 1%; + --color-contrast-medium-l: 48%; + --color-contrast-high: hsl(240, 4%, 20%); + --color-contrast-high-h: 240; + --color-contrast-high-s: 4%; + --color-contrast-high-l: 20%; + --color-contrast-higher: hsl(240, 8%, 12%); + --color-contrast-higher-h: 240; + --color-contrast-higher-s: 8%; + --color-contrast-higher-l: 12% +} + +@supports (--css: variables) { + @media (min-width: 64rem) { + :root { + --space-unit: 1.25em + } + } +} + +:root { + --radius: 0.25em +} + +:root { + --font-primary: sans-serif; + --text-base-size: 1em; + --text-scale-ratio: 1.2; + --text-xs: calc(1em/var(--text-scale-ratio)/var(--text-scale-ratio)); + --text-sm: calc(var(--text-xs)*var(--text-scale-ratio)); + --text-md: calc(var(--text-sm)*var(--text-scale-ratio)*var(--text-scale-ratio)); + --text-lg: calc(var(--text-md)*var(--text-scale-ratio)); + --text-xl: calc(var(--text-lg)*var(--text-scale-ratio)); + --text-xxl: calc(var(--text-xl)*var(--text-scale-ratio)); + --text-xxxl: calc(var(--text-xxl)*var(--text-scale-ratio)); + --body-line-height: 1.4; + --heading-line-height: 1.2; + --font-primary-capital-letter: 1 +} + +@supports (--css: variables) { + @media (min-width: 64rem) { + :root { + --text-base-size: 1.25em; + --text-scale-ratio: 1.25 + } + } +} + +mark { + background-color: hsla(355, 90%, 61%, 0.2); + background-color: hsla(var(--color-accent-h), var(--color-accent-s), var(--color-accent-l), 0.2); + color: inherit +} + +.text-component { + --line-height-multiplier: 1; + --text-vspace-multiplier: 1 +} + +.text-component blockquote { + padding-left: 1em; + border-left: 4px solid hsl(240, 1%, 83%); + border-left: 4px solid var(--color-contrast-low) +} + +.text-component hr { + background: hsl(240, 1%, 83%); + background: var(--color-contrast-low); + height: 1px +} + +.text-component figcaption { + font-size: 0.83333em; + font-size: var(--text-sm); + color: hsl(240, 1%, 48%); + color: var(--color-contrast-medium) +} + +.article.text-component { + --line-height-multiplier: 1.13; + --text-vspace-multiplier: 1.2 +} + +:root { + --btn-font-size: 1em; + --btn-font-size-sm: calc(var(--btn-font-size) - 0.2em); + --btn-font-size-md: calc(var(--btn-font-size) + 0.2em); + --btn-font-size-lg: calc(var(--btn-font-size) + 0.4em); + --btn-radius: 0.25em; + --btn-padding-x: var(--space-sm); + --btn-padding-y: var(--space-xs) +} + +.btn { + --color-shadow: hsla(240, 8%, 12%, 0.15); + --color-shadow: hsla(var(--color-black-h), var(--color-black-s), var(--color-black-l), 0.15); + box-shadow: 0 4px 16px hsla(240, 8%, 12%, 0.15); + box-shadow: 0 4px 16px hsla(var(--color-black-h), var(--color-black-s), var(--color-black-l), 0.15); + cursor: pointer +} + +.btn--primary { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale +} + +.btn--accent { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale +} + +.btn--disabled { + opacity: 0.6 +} + +:root { + --form-control-padding-x: var(--space-sm); + --form-control-padding-y: var(--space-xs); + --form-control-radius: 0.25em +} + +.form-control { + border: 2px solid hsl(240, 1%, 83%); + border: 2px solid var(--color-contrast-low) +} + +.form-control:focus { + outline: none; + border-color: hsl(220, 90%, 56%); + border-color: var(--color-primary); + --color-shadow: hsla(220, 90%, 56%, 0.2); + --color-shadow: hsla(var(--color-primary-h), var(--color-primary-s), var(--color-primary-l), 0.2); + box-shadow: undefined; + box-shadow: 0 0 0 3px var(--color-shadow) +} + +.form-control:focus:focus { + box-shadow: 0 0 0 3px hsla(220, 90%, 56%, 0.2); + box-shadow: 0 0 0 3px var(--color-shadow) +} + +.form-control[aria-invalid="true"] { + border-color: hsl(355, 90%, 61%); + border-color: var(--color-error) +} + +.form-control[aria-invalid="true"]:focus { + --color-shadow: hsla(355, 90%, 61%, 0.2); + --color-shadow: hsla(var(--color-error-h), var(--color-error-s), var(--color-error-l), 0.2); + box-shadow: undefined; + box-shadow: 0 0 0 3px var(--color-shadow) +} + +.form-control[aria-invalid="true"]:focus:focus { + box-shadow: 0 0 0 3px hsla(355, 90%, 61%, 0.2); + box-shadow: 0 0 0 3px var(--color-shadow) +} + +.form-label { + font-size: 0.83333em; + font-size: var(--text-sm) +} + +:root { + --cd-color-1: hsl(206, 21%, 24%); + --cd-color-1-h: 206; + --cd-color-1-s: 21%; + --cd-color-1-l: 24%; + --cd-color-2: hsl(205, 38%, 89%); + --cd-color-2-h: 205; + --cd-color-2-s: 38%; + --cd-color-2-l: 89%; + --cd-color-3: hsl(207, 10%, 55%); + --cd-color-3-h: 207; + --cd-color-3-s: 10%; + --cd-color-3-l: 55%; + --cd-color-4: hsl(111, 51%, 60%); + --cd-color-4-h: 111; + --cd-color-4-s: 51%; + --cd-color-4-l: 60%; + --cd-color-5: hsl(356, 53%, 49%); + --cd-color-5-h: 356; + --cd-color-5-s: 53%; + --cd-color-5-l: 49%; + --cd-color-6: hsl(47, 85%, 61%); + --cd-color-6-h: 47; + --cd-color-6-s: 85%; + --cd-color-6-l: 61%; + --cd-header-height: 200px; + --font-primary: 'Droid Serif', serif; + --font-secondary: 'Open Sans', sans-serif +} + +@supports (--css: variables) { + @media (min-width: 64rem) { + :root { + --cd-header-height: 300px + } + } +} + +.cd-main-header { + height: 200px; + height: var(--cd-header-height); + background: hsl(206, 21%, 24%); + background: var(--cd-color-1); + color: hsl(0, 0%, 100%); + color: var(--color-white); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale +} + +.cd-main-header h1 { + color: inherit +} + +.cd-timeline { + overflow: hidden; + padding: 2em 0; + padding: var(--space-lg) 0; + color: hsl(207, 10%, 55%); + color: var(--cd-color-3); + background-color: hsl(205, 38%, 93.45%); + background-color: hsl(var(--cd-color-2-h), var(--cd-color-2-s), calc(var(--cd-color-2-l)*1.05)); +} + +.cd-timeline h2 { + font-weight: 700 +} + +.cd-timeline__container { + position: relative; + padding: 1.25em 0; + padding: var(--space-md) 0 +} + +.cd-timeline__container::before { + content: ''; + position: absolute; + top: 0; + left: 18px; + height: 100%; + width: 4px; + background: hsl(205, 38%, 89%); + background: var(--cd-color-2) +} + +@media (min-width: 64rem) { + .cd-timeline__container::before { + left: 50%; + -webkit-transform: translateX(-50%); + -ms-transform: translateX(-50%); + transform: translateX(-50%) + } +} + +.cd-timeline__block { + display: -ms-flexbox; + display: flex; + position: relative; + z-index: 1; + margin-bottom: 2em; + margin-bottom: var(--space-lg) +} + +.cd-timeline__block:last-child { + margin-bottom: 0 +} + +@media (min-width: 64rem) { + .cd-timeline__block:nth-child(even) { + -ms-flex-direction: row-reverse; + flex-direction: row-reverse + } +} + +.cd-timeline__img { + display: -ms-flexbox; + display: flex; + -ms-flex-pack: center; + justify-content: center; + -ms-flex-align: center; + align-items: center; + -ms-flex-negative: 0; + flex-shrink: 0; + width: 40px; + height: 40px; + border-radius: 50%; + box-shadow: 0 0 0 4px hsl(0, 0%, 100%), inset 0 2px 0 rgba(0, 0, 0, 0.08), 0 3px 0 4px rgba(0, 0, 0, 0.05); + box-shadow: 0 0 0 4px var(--color-white), inset 0 2px 0 rgba(0, 0, 0, 0.08), 0 3px 0 4px rgba(0, 0, 0, 0.05) +} + +.cd-timeline__img img { + width: 24px; + height: 24px +} + +@media (min-width: 64rem) { + .cd-timeline__img { + width: 60px; + height: 60px; + -ms-flex-order: 1; + order: 1; + margin-left: calc(5% - 30px); + will-change: transform + } + .cd-timeline__block:nth-child(even) .cd-timeline__img { + margin-right: calc(5% - 30px) + } +} + +.cd-timeline__img--picture { + background-color: hsl(111, 51%, 60%); + background-color: var(--cd-color-4) +} + +.cd-timeline__img--movie { + background-color: hsl(356, 53%, 49%); + background-color: var(--cd-color-5) +} + +.cd-timeline__img--location { + background-color: hsl(47, 85%, 61%); + background-color: var(--cd-color-6) +} + +.cd-timeline__content { + -ms-flex-positive: 1; + flex-grow: 1; + position: relative; + margin-left: 1.25em; + margin-left: var(--space-md); + background: hsl(0, 0%, 100%); + background: var(--color-white); + border-radius: 0.25em; + border-radius: var(--radius-md); + padding: 1.25em; + padding: var(--space-md); + box-shadow: 0 3px 0 hsl(205, 38%, 89%); + box-shadow: 0 3px 0 var(--cd-color-2) +} + +.cd-timeline__content::before { + content: ''; + position: absolute; + top: 16px; + right: 100%; + width: 0; + height: 0; + border: 7px solid transparent; + border-right-color: hsl(0, 0%, 100%); + border-right-color: var(--color-white) +} + +.cd-timeline__content h2 { + color: hsl(206, 21%, 24%); + color: var(--cd-color-1) +} + +@media (min-width: 64rem) { + .cd-timeline__content { + width: 45%; + -ms-flex-positive: 0; + flex-grow: 0; + will-change: transform; + margin: 0; + font-size: 0.8em; + --line-height-multiplier: 1.2 + } + .cd-timeline__content::before { + top: 24px + } + .cd-timeline__block:nth-child(odd) .cd-timeline__content::before { + right: auto; + left: 100%; + width: 0; + height: 0; + border: 7px solid transparent; + border-left-color: hsl(0, 0%, 100%); + border-left-color: var(--color-white) + } +} + +.cd-timeline__date { + color: hsla(207, 10%, 55%, 0.7); + color: hsla(var(--cd-color-3-h), var(--cd-color-3-s), var(--cd-color-3-l), 0.7) +} + +@media (min-width: 64rem) { + .cd-timeline__date { + position: absolute; + width: 100%; + left: 120%; + top: 20px + } + .cd-timeline__block:nth-child(even) .cd-timeline__date { + left: auto; + right: 120%; + text-align: right + } +} + +@media (min-width: 64rem) { + .cd-timeline__img--hidden, .cd-timeline__content--hidden { + visibility: hidden + } + .cd-timeline__img--bounce-in { + -webkit-animation: cd-bounce-1 0.6s; + animation: cd-bounce-1 0.6s + } + .cd-timeline__content--bounce-in { + -webkit-animation: cd-bounce-2 0.6s; + animation: cd-bounce-2 0.6s + } + .cd-timeline__block:nth-child(even) .cd-timeline__content--bounce-in { + -webkit-animation-name: cd-bounce-2-inverse; + animation-name: cd-bounce-2-inverse + } +} + +@-webkit-keyframes cd-bounce-1 { + 0% { + opacity: 0; + -webkit-transform: scale(0.5); + transform: scale(0.5) + } + 60% { + opacity: 1; + -webkit-transform: scale(1.2); + transform: scale(1.2) + } + 100% { + -webkit-transform: scale(1); + transform: scale(1) + } +} + +@keyframes cd-bounce-1 { + 0% { + opacity: 0; + -webkit-transform: scale(0.5); + transform: scale(0.5) + } + 60% { + opacity: 1; + -webkit-transform: scale(1.2); + transform: scale(1.2) + } + 100% { + -webkit-transform: scale(1); + transform: scale(1) + } +} + +@-webkit-keyframes cd-bounce-2 { + 0% { + opacity: 0; + -webkit-transform: translateX(-100px); + transform: translateX(-100px) + } + 60% { + opacity: 1; + -webkit-transform: translateX(20px); + transform: translateX(20px) + } + 100% { + -webkit-transform: translateX(0); + transform: translateX(0) + } +} + +@keyframes cd-bounce-2 { + 0% { + opacity: 0; + -webkit-transform: translateX(-100px); + transform: translateX(-100px) + } + 60% { + opacity: 1; + -webkit-transform: translateX(20px); + transform: translateX(20px) + } + 100% { + -webkit-transform: translateX(0); + transform: translateX(0) + } +} + +@-webkit-keyframes cd-bounce-2-inverse { + 0% { + opacity: 0; + -webkit-transform: translateX(100px); + transform: translateX(100px) + } + 60% { + opacity: 1; + -webkit-transform: translateX(-20px); + transform: translateX(-20px) + } + 100% { + -webkit-transform: translateX(0); + transform: translateX(0) + } +} + +@keyframes cd-bounce-2-inverse { + 0% { + opacity: 0; + -webkit-transform: translateX(100px); + transform: translateX(100px) + } + 60% { + opacity: 1; + -webkit-transform: translateX(-20px); + transform: translateX(-20px) + } + 100% { + -webkit-transform: translateX(0); + transform: translateX(0) + } +} -- cgit v1.2.3 From fbeda53cae9db7d66dac63851f41b0e01d26f7ac Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sat, 10 Oct 2020 13:30:35 +0100 Subject: Rename timeline CSS file for easier finding --- pydis_site/templates/home/timeline.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/templates/home/timeline.html b/pydis_site/templates/home/timeline.html index b7f7cda9..73920124 100644 --- a/pydis_site/templates/home/timeline.html +++ b/pydis_site/templates/home/timeline.html @@ -3,7 +3,7 @@ {% block title %}Timeline{% endblock %} {% block head %} - + {% endblock %} -- cgit v1.2.3 From f7fe7c83ab66fbbdb5cd77751190239fe8760c41 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sat, 10 Oct 2020 13:37:38 +0100 Subject: Enable animate in JS --- pydis_site/templates/home/timeline.html | 1 + 1 file changed, 1 insertion(+) diff --git a/pydis_site/templates/home/timeline.html b/pydis_site/templates/home/timeline.html index 73920124..f6b867b2 100644 --- a/pydis_site/templates/home/timeline.html +++ b/pydis_site/templates/home/timeline.html @@ -472,4 +472,5 @@ + {% endblock %} -- cgit v1.2.3 From b42094dd3cae39e137388aa8a4e93d0926b8648f Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sat, 10 Oct 2020 13:39:35 +0100 Subject: Add media query for heading scaling on mobile devices --- pydis_site/static/css/home/timeline.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pydis_site/static/css/home/timeline.css b/pydis_site/static/css/home/timeline.css index dbe49755..f2920686 100644 --- a/pydis_site/static/css/home/timeline.css +++ b/pydis_site/static/css/home/timeline.css @@ -7,6 +7,12 @@ h2 { font-size: 2em; } +@media (max-width: 500px) { + h2 { + font-size: 1em; + } +} + article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section, main, form legend { display: block } -- cgit v1.2.3 From 960748e346cb709b0018921c0ae935bae02b375a Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sat, 10 Oct 2020 13:40:24 +0100 Subject: Reindent HTML --- pydis_site/templates/home/timeline.html | 932 ++++++++++++++++---------------- 1 file changed, 466 insertions(+), 466 deletions(-) diff --git a/pydis_site/templates/home/timeline.html b/pydis_site/templates/home/timeline.html index f6b867b2..c9ff0a27 100644 --- a/pydis_site/templates/home/timeline.html +++ b/pydis_site/templates/home/timeline.html @@ -8,469 +8,469 @@ {% endblock %} {% block content %} - {% include "base/navbar.html" %} - -
-
-
-
- Picture -
- -
-

Python Discord is created

-

lorem ipsum

- -
- Jan 8th, 2017 - Read more -
-
-
- -
- -
- Picture -
- -
-

Python Discord hits 1,000 members

-

lorem ipsum

- -
- Nov 10th, 2017 - Read more -
-
-
- -
- -
- Picture -
- -
-

Our logo is born. Thanks Aperture!

-

picture here?

- -
- Feb 3rd, 2018 - Read more -
-
-
- -
- -
- Picture -
- -
-

PyDis hits 2,000 members; pythondiscord.com and @Python are live

-

lorem ipsum

- -
- Mar 4th, 2018 - Read more -
-
-
- -
- -
- Picture -
- -
-

First code jam with the theme โ€œsnakesโ€

-

lorem ipsum

- -
- Mar 23rd, 2018 - Read more -
-
-
- -
- -
- Picture -
- -
-

The privacy policy is created

-

lorem ipsum

- -
- May 21st, 2018 - Read more -
-
-
- -
- -
- Picture -
- -
-

Do You Even Python and PyDis merger

-

lorem ipsum

- -
- Jun 9th, 2018 - Read more -
-
-
- -
- -
- Picture -
- -
-

PyDis hits 5,000 members

-

lorem ipsum

- -
- Jun 20th, 2018 - Read more -
-
-
- -
- -
- Picture -
- -
-

PyDis is now partnered with Discord; the vanity URL discord.gg/python is created

-

lorem ipsum

- -
- Jul 10th, 2018 - Read more -
-
-
- -
- -
- Picture -
- -
-

First Hacktoberfest PyDis event; @SeasonalBot is created

-

lorem ipsum

- -
- Oct 1st, 2018 - Read more -
-
-
- -
- -
- Picture -
- -
-

PyDis hits 10,000 members

-

lorem ipsum

- -
- Nov 24th, 2018 - Read more -
-
-
- -
- -
- Picture -
- -
-

django-simple-bulma is released on PyPi

-

lorem ipsum

- -
- Dec 19th, 2018 - Read more -
-
-
- -
- -
- Picture -
- -
-

PyDis hits 15,000 members; the โ€œhot ones specialโ€ video is released

-

lorem ipsum

- -
- Apr 8th, 2019 - Read more -
-
-
- -
- -
- Picture -
- -
-

the Django rewrite of pythondiscord.com is now live!

-

lorem ipsum

- -
- Sep 15, 2019 - Read more -
-
-
- -
- -
- Picture -
- -
-

flake8-annotations is released on PyPI

-

lorem ipsum

- -
- Sep 25th, 2019 - Read more -
-
-
- -
- -
- Picture -
- -
-

The code of conduct is created

-

lorem ipsum

- -
- Oct 26th, 2019 - Read more -
-
-
- -
- -
- Picture -
- -
-

PyDis hits 30,000 members

-

lorem ipsum

- -
- Dec 22nd, 2019 - Read more -
-
-
- -
- -
- Picture -
- -
-

PyDis sixth code jam with the theme โ€œAncient technologyโ€ and the technology Kivy

-

lorem ipsum

- -
- Jan 17, 2020 - Read more -
-
-
- -
- -
- Picture -
- -
-

The new help channel system is live

-

lorem ipsum

- -
- Apr 5th, 2020 - Read more -
-
-
- -
- -
- Picture -
- -
-

Python Discord hits 40,000 members, and is now bigger than Liechtenstein.

-

picture here?

- -
- Apr 14, 2020 - Read more -
-
-
- -
- -
- Picture -
- -
-

PyDis Game Jam 2020 with the โ€œThree of a Kindโ€ theme and Arcade as the technology

-

lorem ipsum

- -
- Apr 17th, 2020 - Read more -
-
-
- -
- -
- Picture -
- -
-

ModMail is now live

-

lorem ipsum

- -
- May 25th, 2020 - Read more -
-
-
- -
- -
- Picture -
- -
-

Python Discord is now listed on python.org/community

-

lorem ipsum

- -
- May 28th, 2020 - Read more -
-
-
- -
- -
- Picture -
- -
-

Python Discord Public Statistics are now live

-

lorem ipsum

- -
- Jun 4th, 2020 - Read more -
-
-
- -
- -
- Picture -
- -
-

PyDis summer code jam 2020 with the theme โ€œEarly Internetโ€ and Django as the technology

-

lorem ipsum

- -
- Jul 31st, 2020 - Read more -
-
-
- -
- -
- Picture -
- -
-

Python Discord is now the new home of the PyWeek event!

-

lorem ipsum

- -
- Aug 16th, 2020 - Read more -
-
-
- -
- -
- Picture -
- -
-

Python Discord hits 100,000 members.

-

lorem ipsum

- -
- Sep ??, 2020 - Read more -
-
-
-
-
- - - {% endblock %} +{% include "base/navbar.html" %} + +
+
+
+
+ Picture +
+ +
+

Python Discord is created

+

lorem ipsum

+ +
+ Jan 8th, 2017 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

Python Discord hits 1,000 members

+

lorem ipsum

+ +
+ Nov 10th, 2017 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

Our logo is born. Thanks Aperture!

+

picture here?

+ +
+ Feb 3rd, 2018 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

PyDis hits 2,000 members; pythondiscord.com and @Python are live

+

lorem ipsum

+ +
+ Mar 4th, 2018 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

First code jam with the theme โ€œsnakesโ€

+

lorem ipsum

+ +
+ Mar 23rd, 2018 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

The privacy policy is created

+

lorem ipsum

+ +
+ May 21st, 2018 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

Do You Even Python and PyDis merger

+

lorem ipsum

+ +
+ Jun 9th, 2018 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

PyDis hits 5,000 members

+

lorem ipsum

+ +
+ Jun 20th, 2018 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

PyDis is now partnered with Discord; the vanity URL discord.gg/python is created

+

lorem ipsum

+ +
+ Jul 10th, 2018 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

First Hacktoberfest PyDis event; @SeasonalBot is created

+

lorem ipsum

+ +
+ Oct 1st, 2018 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

PyDis hits 10,000 members

+

lorem ipsum

+ +
+ Nov 24th, 2018 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

django-simple-bulma is released on PyPi

+

lorem ipsum

+ +
+ Dec 19th, 2018 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

PyDis hits 15,000 members; the โ€œhot ones specialโ€ video is released

+

lorem ipsum

+ +
+ Apr 8th, 2019 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

the Django rewrite of pythondiscord.com is now live!

+

lorem ipsum

+ +
+ Sep 15, 2019 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

flake8-annotations is released on PyPI

+

lorem ipsum

+ +
+ Sep 25th, 2019 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

The code of conduct is created

+

lorem ipsum

+ +
+ Oct 26th, 2019 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

PyDis hits 30,000 members

+

lorem ipsum

+ +
+ Dec 22nd, 2019 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

PyDis sixth code jam with the theme โ€œAncient technologyโ€ and the technology Kivy

+

lorem ipsum

+ +
+ Jan 17, 2020 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

The new help channel system is live

+

lorem ipsum

+ +
+ Apr 5th, 2020 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

Python Discord hits 40,000 members, and is now bigger than Liechtenstein.

+

picture here?

+ +
+ Apr 14, 2020 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

PyDis Game Jam 2020 with the โ€œThree of a Kindโ€ theme and Arcade as the technology

+

lorem ipsum

+ +
+ Apr 17th, 2020 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

ModMail is now live

+

lorem ipsum

+ +
+ May 25th, 2020 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

Python Discord is now listed on python.org/community

+

lorem ipsum

+ +
+ May 28th, 2020 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

Python Discord Public Statistics are now live

+

lorem ipsum

+ +
+ Jun 4th, 2020 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

PyDis summer code jam 2020 with the theme โ€œEarly Internetโ€ and Django as the technology

+

lorem ipsum

+ +
+ Jul 31st, 2020 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

Python Discord is now the new home of the PyWeek event!

+

lorem ipsum

+ +
+ Aug 16th, 2020 + Read more +
+
+
+ +
+ +
+ Picture +
+ +
+

Python Discord hits 100,000 members.

+

lorem ipsum

+ +
+ Sep ??, 2020 + Read more +
+
+
+
+
+ + +{% endblock %} -- cgit v1.2.3 From 392be623a573a845ca4c4af5228250192a7f16b1 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sat, 10 Oct 2020 13:41:26 +0100 Subject: Remove read more buttons --- pydis_site/templates/home/timeline.html | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/pydis_site/templates/home/timeline.html b/pydis_site/templates/home/timeline.html index c9ff0a27..8ad19f4b 100644 --- a/pydis_site/templates/home/timeline.html +++ b/pydis_site/templates/home/timeline.html @@ -23,7 +23,6 @@
Jan 8th, 2017 - Read more
@@ -40,7 +39,6 @@
Nov 10th, 2017 - Read more
@@ -57,7 +55,6 @@
Feb 3rd, 2018 - Read more
@@ -74,7 +71,6 @@
Mar 4th, 2018 - Read more
@@ -91,7 +87,6 @@
Mar 23rd, 2018 - Read more
@@ -108,7 +103,6 @@
May 21st, 2018 - Read more
@@ -125,7 +119,6 @@
Jun 9th, 2018 - Read more
@@ -142,7 +135,6 @@
Jun 20th, 2018 - Read more
@@ -159,7 +151,6 @@
Jul 10th, 2018 - Read more
@@ -176,7 +167,6 @@
Oct 1st, 2018 - Read more
@@ -193,7 +183,6 @@
Nov 24th, 2018 - Read more
@@ -210,7 +199,6 @@
Dec 19th, 2018 - Read more
@@ -227,7 +215,6 @@
Apr 8th, 2019 - Read more
@@ -244,7 +231,6 @@
Sep 15, 2019 - Read more
@@ -261,7 +247,6 @@
Sep 25th, 2019 - Read more
@@ -278,7 +263,6 @@
Oct 26th, 2019 - Read more
@@ -295,7 +279,6 @@
Dec 22nd, 2019 - Read more
@@ -312,7 +295,6 @@
Jan 17, 2020 - Read more
@@ -329,7 +311,6 @@
Apr 5th, 2020 - Read more
@@ -346,7 +327,6 @@
Apr 14, 2020 - Read more
@@ -363,7 +343,6 @@
Apr 17th, 2020 - Read more
@@ -380,7 +359,6 @@
May 25th, 2020 - Read more
@@ -397,7 +375,6 @@
May 28th, 2020 - Read more
@@ -414,7 +391,6 @@
Jun 4th, 2020 - Read more
@@ -431,7 +407,6 @@
Jul 31st, 2020 - Read more
@@ -448,7 +423,6 @@
Aug 16th, 2020 - Read more
@@ -465,7 +439,6 @@
Sep ??, 2020 - Read more
-- cgit v1.2.3 From 9c812d5c40813b453bf0711538eaebea46e2b16b Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 10 Oct 2020 19:09:31 +0300 Subject: Add voice ban to infraction types and create migration for it --- .../migrations/0067_add_voice_ban_infraction_type.py | 18 ++++++++++++++++++ pydis_site/apps/api/models/bot/infraction.py | 3 ++- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 pydis_site/apps/api/migrations/0067_add_voice_ban_infraction_type.py diff --git a/pydis_site/apps/api/migrations/0067_add_voice_ban_infraction_type.py b/pydis_site/apps/api/migrations/0067_add_voice_ban_infraction_type.py new file mode 100644 index 00000000..9a940ff4 --- /dev/null +++ b/pydis_site/apps/api/migrations/0067_add_voice_ban_infraction_type.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.10 on 2020-10-10 16:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0066_merge_20201003_0730'), + ] + + operations = [ + migrations.AlterField( + model_name='infraction', + name='type', + field=models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('watch', 'Watch'), ('mute', 'Mute'), ('kick', 'Kick'), ('ban', 'Ban'), ('superstar', 'Superstar'), ('voice_ban', 'Voice Ban')], help_text='The type of the infraction.', max_length=9), + ), + ] diff --git a/pydis_site/apps/api/models/bot/infraction.py b/pydis_site/apps/api/models/bot/infraction.py index 7660cbba..5ef3a4ce 100644 --- a/pydis_site/apps/api/models/bot/infraction.py +++ b/pydis_site/apps/api/models/bot/infraction.py @@ -15,7 +15,8 @@ class Infraction(ModelReprMixin, models.Model): ("mute", "Mute"), ("kick", "Kick"), ("ban", "Ban"), - ("superstar", "Superstar") + ("superstar", "Superstar"), + ("voice_ban", "Voice Ban") ) inserted_at = models.DateTimeField( default=timezone.now, -- cgit v1.2.3 From 511152c0a91ae81949941ff9d8f8129f01338173 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 10 Oct 2020 19:14:36 +0300 Subject: Don't allow voice ban to be hidden infraction --- pydis_site/apps/api/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 25c5c82e..10eb3839 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -167,7 +167,7 @@ class InfractionSerializer(ModelSerializer): raise ValidationError({'expires_at': [f'{infr_type} infractions cannot expire.']}) hidden = attrs.get('hidden') - if hidden and infr_type in ('superstar', 'warning'): + if hidden and infr_type in ('superstar', 'warning', 'voice_ban'): raise ValidationError({'hidden': [f'{infr_type} infractions cannot be hidden.']}) if not hidden and infr_type in ('note', ): -- cgit v1.2.3 From 990d8cfc98ec71a31ea3ef659cb109547f987e31 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 10 Oct 2020 19:56:54 +0300 Subject: Include voice ban status on metricity data --- pydis_site/apps/api/viewsets/bot/user.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 3ab71186..19f55555 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -1,6 +1,7 @@ import typing from collections import OrderedDict +from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist from rest_framework import status from rest_framework.decorators import action from rest_framework.pagination import PageNumberPagination @@ -9,6 +10,7 @@ from rest_framework.response import Response from rest_framework.serializers import ModelSerializer from rest_framework.viewsets import ModelViewSet +from pydis_site.apps.api.models.bot.infraction import Infraction from pydis_site.apps.api.models.bot.metricity import Metricity, NotFound from pydis_site.apps.api.models.bot.user import User from pydis_site.apps.api.serializers import UserSerializer @@ -240,10 +242,21 @@ class UserViewSet(ModelViewSet): def metricity_data(self, request: Request, pk: str = None) -> Response: """Request handler for metricity_data endpoint.""" user = self.get_object() + + try: + Infraction.objects.get(user__id=user.id, active=True, type="voice_ban") + except ObjectDoesNotExist: + voice_banned = False + except MultipleObjectReturned: + voice_banned = True + else: + voice_banned = True + with Metricity() as metricity: try: data = metricity.user(user.id) data["total_messages"] = metricity.total_messages(user.id) + data["voice_banned"] = voice_banned return Response(data, status=status.HTTP_200_OK) except NotFound: return Response(dict(detail="User not found in metricity"), -- cgit v1.2.3 From f1bdb525af330f28742638fdaa19cd8c76235906 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 10 Oct 2020 20:15:01 +0300 Subject: Cover metricity voice ban status with tests --- pydis_site/apps/api/tests/test_users.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py index d03785ae..01bf208b 100644 --- a/pydis_site/apps/api/tests/test_users.py +++ b/pydis_site/apps/api/tests/test_users.py @@ -1,5 +1,6 @@ from unittest.mock import patch +from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist from django_hosts.resolvers import reverse from .base import APISubdomainTestCase @@ -419,6 +420,7 @@ class UserMetricityTests(APISubdomainTestCase): self.assertEqual(response.json(), { "verified_at": verified_at, "total_messages": total_messages, + "voice_banned": False, }) def test_no_metricity_user(self): @@ -432,6 +434,27 @@ class UserMetricityTests(APISubdomainTestCase): # Then self.assertEqual(response.status_code, 404) + def test_metricity_voice_banned(self): + cases = [ + {'exception': None, 'voice_banned': True}, + {'exception': ObjectDoesNotExist, 'voice_banned': False}, + {'exception': MultipleObjectsReturned, 'voice_banned': True}, + ] + + self.mock_metricity_user("foo", 1) + + for case in cases: + with self.subTest(exception=case['exception'], voice_banned=case['voice_banned']): + with patch("pydis_site.apps.api.viewsets.bot.user.Infraction.objects.get") as p: + p.side_effect = case['exception'] + + url = reverse('bot:user-metricity-data', args=[0], host='api') + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()["voice_banned"], case["voice_banned"]) + + def mock_metricity_user(self, verified_at, total_messages): patcher = patch("pydis_site.apps.api.viewsets.bot.user.Metricity") self.metricity = patcher.start() -- cgit v1.2.3 From d80c7f18fadd1abc1aa2c080c2de6e7dee395883 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 10 Oct 2020 20:19:53 +0300 Subject: Fix wrong exception name --- pydis_site/apps/api/viewsets/bot/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 19f55555..367f6b65 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -247,7 +247,7 @@ class UserViewSet(ModelViewSet): Infraction.objects.get(user__id=user.id, active=True, type="voice_ban") except ObjectDoesNotExist: voice_banned = False - except MultipleObjectReturned: + except MultipleObjectsReturned: voice_banned = True else: voice_banned = True -- cgit v1.2.3 From 8cb18e635920d64734a66be77e849cae3441108b Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 10 Oct 2020 20:21:42 +0300 Subject: Remove unnecessary newline from user tests --- pydis_site/apps/api/tests/test_users.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py index 01bf208b..e512f506 100644 --- a/pydis_site/apps/api/tests/test_users.py +++ b/pydis_site/apps/api/tests/test_users.py @@ -454,7 +454,6 @@ class UserMetricityTests(APISubdomainTestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response.json()["voice_banned"], case["voice_banned"]) - def mock_metricity_user(self, verified_at, total_messages): patcher = patch("pydis_site.apps.api.viewsets.bot.user.Metricity") self.metricity = patcher.start() -- cgit v1.2.3 From 74f2131d05fdaa27b841e32f6c2fce2926b60f70 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 10 Oct 2020 20:34:13 +0300 Subject: Add trailing comma to infraction types listing Co-authored-by: Joe Banks --- pydis_site/apps/api/models/bot/infraction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/apps/api/models/bot/infraction.py b/pydis_site/apps/api/models/bot/infraction.py index 5ef3a4ce..60c1e8dd 100644 --- a/pydis_site/apps/api/models/bot/infraction.py +++ b/pydis_site/apps/api/models/bot/infraction.py @@ -16,7 +16,7 @@ class Infraction(ModelReprMixin, models.Model): ("kick", "Kick"), ("ban", "Ban"), ("superstar", "Superstar"), - ("voice_ban", "Voice Ban") + ("voice_ban", "Voice Ban"), ) inserted_at = models.DateTimeField( default=timezone.now, -- cgit v1.2.3 From 9c4ed0b98e4f34fe3a589e4e26f2b31e6c5169f9 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 10 Oct 2020 20:35:32 +0300 Subject: Don't catch multiple object returned exception --- pydis_site/apps/api/viewsets/bot/user.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 367f6b65..5205dc97 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -1,7 +1,7 @@ import typing from collections import OrderedDict -from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist +from django.core.exceptions import ObjectDoesNotExist from rest_framework import status from rest_framework.decorators import action from rest_framework.pagination import PageNumberPagination @@ -247,8 +247,6 @@ class UserViewSet(ModelViewSet): Infraction.objects.get(user__id=user.id, active=True, type="voice_ban") except ObjectDoesNotExist: voice_banned = False - except MultipleObjectsReturned: - voice_banned = True else: voice_banned = True -- cgit v1.2.3 From 42e34e955724eb47a505e24bcd03dc55c8e66419 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 10 Oct 2020 20:36:18 +0300 Subject: Remove multiple objects returning test case --- pydis_site/apps/api/tests/test_users.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py index e512f506..72ffcb3c 100644 --- a/pydis_site/apps/api/tests/test_users.py +++ b/pydis_site/apps/api/tests/test_users.py @@ -1,6 +1,6 @@ from unittest.mock import patch -from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist +from django.core.exceptions import ObjectDoesNotExist from django_hosts.resolvers import reverse from .base import APISubdomainTestCase @@ -438,7 +438,6 @@ class UserMetricityTests(APISubdomainTestCase): cases = [ {'exception': None, 'voice_banned': True}, {'exception': ObjectDoesNotExist, 'voice_banned': False}, - {'exception': MultipleObjectsReturned, 'voice_banned': True}, ] self.mock_metricity_user("foo", 1) -- cgit v1.2.3 From e982e2841da532e3eb215926277a4c3d17e47e81 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Mon, 12 Oct 2020 00:24:10 +0100 Subject: Add copy from lemon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Leon Sandรธy --- pydis_site/templates/home/timeline.html | 60 ++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/pydis_site/templates/home/timeline.html b/pydis_site/templates/home/timeline.html index 8ad19f4b..4d837b30 100644 --- a/pydis_site/templates/home/timeline.html +++ b/pydis_site/templates/home/timeline.html @@ -19,7 +19,7 @@

Python Discord is created

-

lorem ipsum

+

joe becomes one of the owners around 3 days after it is created, and lemon joins the owner team later in the year, when the community has around 300 members.

Jan 8th, 2017 @@ -35,7 +35,7 @@

Python Discord hits 1,000 members

-

lorem ipsum

+

Our main source of new users at this point is a post on Reddit that happens to get very good SEO. We are one of the top 10 search engine hits for the search term "python discord".

Nov 10th, 2017 @@ -50,7 +50,7 @@
-

Our logo is born. Thanks Aperture!

+

Our logo is born. Thanks @Aperture!

picture here?

@@ -67,7 +67,7 @@

PyDis hits 2,000 members; pythondiscord.com and @Python are live

-

lorem ipsum

+

The public moderation bot we're using at the time, Rowboat, announces it will be shutting down. We decide that we'll write our own bot to handle moderation, so that we can have more control over its features. We also buy a domain and start making a website in Flask.

Mar 4th, 2018 @@ -83,7 +83,7 @@

First code jam with the theme โ€œsnakesโ€

-

lorem ipsum

+

Our very first Code Jam attracts a handful of users who work in random teams of 2. We ask our participants to write a snake-themed Discord bot. Most of the code written for this jam still lives on in SeasonalBot, and you can play with it by using the .snakes command. For more information on this event, see the event page

Mar 23rd, 2018 @@ -99,7 +99,7 @@

The privacy policy is created

-

lorem ipsum

+

Since data privacy is quite important to us, we create a privacy page pretty much as soon as our new bot and site starts collecting some data. To this day, we keep our privacy policy up to date with all changes, and since April 2020 we've started doing monthly data reviews.

May 21st, 2018 @@ -115,7 +115,7 @@

Do You Even Python and PyDis merger

-

lorem ipsum

+

At this point in time, there are only two serious Python communities on Discord - Ours, and one called Do You Even Python. We approach the owners of DYEP with a bold proposal - let's shut down their community, replace it with links to ours, and in return we will let their staff join our staff. This gives us a big boost in members, and eventually leads to @eivl and @Mr. Hemlock joining our Admin team

Jun 9th, 2018 @@ -130,8 +130,8 @@
-

PyDis hits 5,000 members

-

lorem ipsum

+

PyDis hits 5,000 members and partners with r/Python

+

As we continue to grow, we approach the r/Python subreddit and ask to become their official Discord community. They agree, and we become listed in their sidebar, giving us yet another source of new members.

Jun 20th, 2018 @@ -147,7 +147,7 @@

PyDis is now partnered with Discord; the vanity URL discord.gg/python is created

-

lorem ipsum

+

After being rejected for their Partner program several times, we finally get approved. The recent partnership with the r/Python subreddit plays a significant role in qualifying us for this partnership.

Jul 10th, 2018 @@ -163,7 +163,7 @@

First Hacktoberfest PyDis event; @SeasonalBot is created

-

lorem ipsum

+

We create a second bot for our community and fill it up with simple, fun and relatively easy issues. The idea is to create an approachable arena for our members to cut their open-source teeth on, and to provide lots of help and hand-holding for those who get stuck. We're training our members to be productive contributors in the open-source ecosystem.

Oct 1st, 2018 @@ -179,7 +179,7 @@

PyDis hits 10,000 members

-

lorem ipsum

+

We partner with RLBot, move from GitLab to GitHub, and start putting together the first Advent of Code event.

Nov 24th, 2018 @@ -195,7 +195,7 @@

django-simple-bulma is released on PyPi

-

lorem ipsum

+

Our very first package on PyPI, django-simple-bulma is a package that sets up the Bulma CSS framework for your Django application and lets you configure everything in settings.py.

Dec 19th, 2018 @@ -211,7 +211,7 @@

PyDis hits 15,000 members; the โ€œhot ones specialโ€ video is released

-

lorem ipsum

+
Apr 8th, 2019 @@ -226,8 +226,8 @@
-

the Django rewrite of pythondiscord.com is now live!

-

lorem ipsum

+

The Django rewrite of pythondiscord.com is now live!

+

The site is getting more and more complex, and it's time for a rewrite. We decide to go for a different stack, and build a website based on Django, DRF, Bulma and PostgreSQL.

Sep 15, 2019 @@ -259,7 +259,7 @@

The code of conduct is created

-

lorem ipsum

+

Inspired by the Adafruit, Rust and Django communities, an essential community pillar is created; Our Code of Conduct.

Oct 26th, 2019 @@ -275,7 +275,7 @@

PyDis hits 30,000 members

-

lorem ipsum

+

More than tripling in size since the year before, the community hits 30000 users. At this point, we're probably the largest Python chat community on the planet.

Dec 22nd, 2019 @@ -291,7 +291,9 @@

PyDis sixth code jam with the theme โ€œAncient technologyโ€ and the technology Kivy

-

lorem ipsum

+

Our Code Jams are becoming an increasingly big deal, and the Kivy core developers join us to judge the event and help out our members during the event. One of them, @tshirtman, even joins our staff!

+ +
Jan 17, 2020 @@ -307,7 +309,7 @@

The new help channel system is live

-

lorem ipsum

+

We release our dynamic help-channel system, which allows you to claim your very own help channel instead of fighting over the static help channels. We release a Help Channel Guide to help our members fully understand how the system works.

Apr 5th, 2020 @@ -339,7 +341,9 @@

PyDis Game Jam 2020 with the โ€œThree of a Kindโ€ theme and Arcade as the technology

-

lorem ipsum

+

The creator of Arcade, Paul Vincent Craven, joins us as a judge. Several of the Code Jam participants also end up getting involved contributing to the Arcade repository.

+ +
Apr 17th, 2020 @@ -355,7 +359,7 @@

ModMail is now live

-

lorem ipsum

+

Having originally planned to write our own ModMail bot from scratch, we come across an exceptionally good ModMail bot by kyb3r and decide to just self-host that one instead.

May 25th, 2020 @@ -371,7 +375,7 @@

Python Discord is now listed on python.org/community

-

lorem ipsum

+

After working towards this goal for months, we finally work out an arrangement with the PSF that allows us to be listed on that most holiest of websites: https://python.org/. There was much rejoicing.

May 28th, 2020 @@ -387,7 +391,7 @@

Python Discord Public Statistics are now live

-

lorem ipsum

+

After getting numerous requests to publish beautiful data on member count and channel use, we create https://stats.pythondiscord.com/ for all to enjoy.

Jun 4th, 2020 @@ -403,7 +407,9 @@

PyDis summer code jam 2020 with the theme โ€œEarly Internetโ€ and Django as the technology

-

lorem ipsum

+

Sponsored by the Django Software Foundation and JetBrains, the Summer Code Jam for 2020 attracts hundreds of participants, and sees the creation of some fantastic projects. Check them out in our judge stream below:

+ +
Jul 31st, 2020 @@ -419,7 +425,7 @@

Python Discord is now the new home of the PyWeek event!

-

lorem ipsum

+

PyWeek, a game jam that has been running since 2005, joins Python Discord as one of our official events. Find more information about PyWeek on their official website.

Aug 16th, 2020 @@ -435,7 +441,7 @@

Python Discord hits 100,000 members.

-

lorem ipsum

+

After years of hard work, we hit 100,000 users. A monumental milestone, and one we're very proud of. To commemorate it, we create this timeline.

Sep ??, 2020 -- cgit v1.2.3 From de3b67c562fa90eac2d79938481a5b84b4e1079f Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Mon, 12 Oct 2020 00:30:41 +0100 Subject: Remove pointless comments --- pydis_site/templates/home/timeline.html | 314 ++++++++++++++++++-------------- 1 file changed, 175 insertions(+), 139 deletions(-) diff --git a/pydis_site/templates/home/timeline.html b/pydis_site/templates/home/timeline.html index 4d837b30..da89c25e 100644 --- a/pydis_site/templates/home/timeline.html +++ b/pydis_site/templates/home/timeline.html @@ -15,39 +15,41 @@
Picture -
+

Python Discord is created

-

joe becomes one of the owners around 3 days after it is created, and lemon joins the owner team later in the year, when the community has around 300 members.

+

joe becomes one of the owners around 3 days after it + is created, and lemon joins the owner team later in the year, when the community + has around 300 members.

Jan 8th, 2017
-
-
+
+
-
Picture -
+

Python Discord hits 1,000 members

-

Our main source of new users at this point is a post on Reddit that happens to get very good SEO. We are one of the top 10 search engine hits for the search term "python discord".

+

Our main source of new users at this point is a post on Reddit that + happens to get very good SEO. We are one of the top 10 search engine hits for the search term + "python discord".

Nov 10th, 2017
-
-
+
+
-
Picture -
+

Our logo is born. Thanks @Aperture!

@@ -56,190 +58,208 @@
Feb 3rd, 2018
-
-
+
+
-
Picture -
+

PyDis hits 2,000 members; pythondiscord.com and @Python are live

-

The public moderation bot we're using at the time, Rowboat, announces it will be shutting down. We decide that we'll write our own bot to handle moderation, so that we can have more control over its features. We also buy a domain and start making a website in Flask.

+

The public moderation bot we're using at the time, Rowboat, announces + it will be shutting down. We decide that we'll write our own bot to handle moderation, so that we + can have more control over its features. We also buy a domain and start making a website in Flask. +

Mar 4th, 2018
-
-
+
+
-
Picture -
+

First code jam with the theme โ€œsnakesโ€

-

Our very first Code Jam attracts a handful of users who work in random teams of 2. We ask our participants to write a snake-themed Discord bot. Most of the code written for this jam still lives on in SeasonalBot, and you can play with it by using the .snakes command. For more information on this event, see the event page

+

Our very first Code Jam attracts a handful of users who work in random + teams of 2. We ask our participants to write a snake-themed Discord bot. Most of the code written + for this jam still lives on in SeasonalBot, and you can play with it by using the + .snakes command. For more information on this event, see the event page

Mar 23rd, 2018
-
-
+
+
-
Picture -
+

The privacy policy is created

-

Since data privacy is quite important to us, we create a privacy page pretty much as soon as our new bot and site starts collecting some data. To this day, we keep our privacy policy up to date with all changes, and since April 2020 we've started doing monthly data reviews.

+

Since data privacy is quite important to us, we create a privacy page + pretty much as soon as our new bot and site starts collecting some data. To this day, we keep our privacy policy up to date with all + changes, and since April 2020 we've started doing monthly data reviews.

May 21st, 2018
-
-
+
+
-
Picture -
+

Do You Even Python and PyDis merger

-

At this point in time, there are only two serious Python communities on Discord - Ours, and one called Do You Even Python. We approach the owners of DYEP with a bold proposal - let's shut down their community, replace it with links to ours, and in return we will let their staff join our staff. This gives us a big boost in members, and eventually leads to @eivl and @Mr. Hemlock joining our Admin team

+

At this point in time, there are only two serious Python communities on + Discord - Ours, and one called Do You Even Python. We approach the owners of DYEP with a bold + proposal - let's shut down their community, replace it with links to ours, and in return we will let + their staff join our staff. This gives us a big boost in members, and eventually leads to @eivl and + @Mr. Hemlock joining our Admin team

Jun 9th, 2018
-
-
+
+
-
Picture -
+

PyDis hits 5,000 members and partners with r/Python

-

As we continue to grow, we approach the r/Python subreddit and ask to become their official Discord community. They agree, and we become listed in their sidebar, giving us yet another source of new members.

+

As we continue to grow, we approach the r/Python subreddit and ask to + become their official Discord community. They agree, and we become listed in their sidebar, giving + us yet another source of new members.

Jun 20th, 2018
-
-
+
+
-
Picture -
+

PyDis is now partnered with Discord; the vanity URL discord.gg/python is created

-

After being rejected for their Partner program several times, we finally get approved. The recent partnership with the r/Python subreddit plays a significant role in qualifying us for this partnership.

+

After being rejected for their Partner program several times, we + finally get approved. The recent partnership with the r/Python subreddit plays a significant role in + qualifying us for this partnership.

Jul 10th, 2018
-
-
+
+
-
Picture -
+

First Hacktoberfest PyDis event; @SeasonalBot is created

-

We create a second bot for our community and fill it up with simple, fun and relatively easy issues. The idea is to create an approachable arena for our members to cut their open-source teeth on, and to provide lots of help and hand-holding for those who get stuck. We're training our members to be productive contributors in the open-source ecosystem.

+

We create a second bot for our community and fill it up with simple, + fun and relatively easy issues. The idea is to create an approachable arena for our members to cut + their open-source teeth on, and to provide lots of help and hand-holding for those who get stuck. + We're training our members to be productive contributors in the open-source ecosystem.

Oct 1st, 2018
-
-
+
+
-
Picture -
+

PyDis hits 10,000 members

-

We partner with RLBot, move from GitLab to GitHub, and start putting together the first Advent of Code event.

+

We partner with RLBot, move from GitLab to GitHub, and start putting + together the first Advent of Code event.

Nov 24th, 2018
-
-
+
+
-
Picture -
+

django-simple-bulma is released on PyPi

-

Our very first package on PyPI, django-simple-bulma is a package that sets up the Bulma CSS framework for your Django application and lets you configure everything in settings.py.

+

Our very first package on PyPI, django-simple-bulma is a package that + sets up the Bulma CSS framework for your Django application and lets you configure everything in + settings.py.

Dec 19th, 2018
-
-
+
+
-
Picture -
+

PyDis hits 15,000 members; the โ€œhot ones specialโ€ video is released

- +
Apr 8th, 2019
-
-
+
+
-
Picture -
+

The Django rewrite of pythondiscord.com is now live!

-

The site is getting more and more complex, and it's time for a rewrite. We decide to go for a different stack, and build a website based on Django, DRF, Bulma and PostgreSQL.

+

The site is getting more and more complex, and it's time for a rewrite. + We decide to go for a different stack, and build a website based on Django, DRF, Bulma and + PostgreSQL.

Sep 15, 2019
-
-
+
+
-
Picture -
+

flake8-annotations is released on PyPI

@@ -248,208 +268,224 @@
Sep 25th, 2019
-
-
+
+
-
Picture -
+

The code of conduct is created

-

Inspired by the Adafruit, Rust and Django communities, an essential community pillar is created; Our Code of Conduct.

+

Inspired by the Adafruit, Rust and Django communities, an essential + community pillar is created; Our Code of + Conduct.

Oct 26th, 2019
-
-
+
+
-
Picture -
+

PyDis hits 30,000 members

-

More than tripling in size since the year before, the community hits 30000 users. At this point, we're probably the largest Python chat community on the planet.

+

More than tripling in size since the year before, the community hits + 30000 users. At this point, we're probably the largest Python chat community on the planet.

Dec 22nd, 2019
-
- + +
-
Picture -
+

PyDis sixth code jam with the theme โ€œAncient technologyโ€ and the technology Kivy

-

Our Code Jams are becoming an increasingly big deal, and the Kivy core developers join us to judge the event and help out our members during the event. One of them, @tshirtman, even joins our staff!

- - +

Our Code Jams are becoming an increasingly big deal, and the Kivy core + developers join us to judge the event and help out our members during the event. One of them, + @tshirtman, even joins our staff!

+ +
Jan 17, 2020
-
- + +
-
Picture -
+

The new help channel system is live

-

We release our dynamic help-channel system, which allows you to claim your very own help channel instead of fighting over the static help channels. We release a Help Channel Guide to help our members fully understand how the system works.

+

We release our dynamic help-channel system, which allows you to claim + your very own help channel instead of fighting over the static help channels. We release a Help Channel Guide to + help our members fully understand how the system works.

Apr 5th, 2020
-
- + +
-
Picture -
+

Python Discord hits 40,000 members, and is now bigger than Liechtenstein.

-

picture here?

+

+

Apr 14, 2020
-
- + +
-
Picture -
+

PyDis Game Jam 2020 with the โ€œThree of a Kindโ€ theme and Arcade as the technology

-

The creator of Arcade, Paul Vincent Craven, joins us as a judge. Several of the Code Jam participants also end up getting involved contributing to the Arcade repository.

- - +

The creator of Arcade, Paul Vincent Craven, joins us as a judge. + Several of the Code Jam participants also end up getting involved contributing to the Arcade + repository.

+ +
Apr 17th, 2020
-
- + +
-
Picture -
+

ModMail is now live

-

Having originally planned to write our own ModMail bot from scratch, we come across an exceptionally good ModMail bot by kyb3r and decide to just self-host that one instead.

+

Having originally planned to write our own ModMail bot from scratch, we + come across an exceptionally good ModMail bot by + kyb3r and decide to just self-host that one instead.

May 25th, 2020
-
- + +
-
Picture -
+

Python Discord is now listed on python.org/community

-

After working towards this goal for months, we finally work out an arrangement with the PSF that allows us to be listed on that most holiest of websites: https://python.org/. There was much rejoicing.

+

After working towards this goal for months, we finally work out an + arrangement with the PSF that allows us to be listed on that most holiest of websites: + https://python.org/. There was much rejoicing.

May 28th, 2020
-
- + +
-
Picture -
+

Python Discord Public Statistics are now live

-

After getting numerous requests to publish beautiful data on member count and channel use, we create https://stats.pythondiscord.com/ for all to enjoy.

+

After getting numerous requests to publish beautiful data on member + count and channel use, we create https://stats.pythondiscord.com/ for all to enjoy.

Jun 4th, 2020
-
- + +
-
Picture -
+

PyDis summer code jam 2020 with the theme โ€œEarly Internetโ€ and Django as the technology

-

Sponsored by the Django Software Foundation and JetBrains, the Summer Code Jam for 2020 attracts hundreds of participants, and sees the creation of some fantastic projects. Check them out in our judge stream below:

- - +

Sponsored by the Django Software Foundation and JetBrains, the Summer + Code Jam for 2020 attracts hundreds of participants, and sees the creation of some fantastic + projects. Check them out in our judge stream below:

+ +
Jul 31st, 2020
-
- + +
-
Picture -
+

Python Discord is now the new home of the PyWeek event!

-

PyWeek, a game jam that has been running since 2005, joins Python Discord as one of our official events. Find more information about PyWeek on their official website.

+

PyWeek, a game jam that has been running since 2005, joins Python + Discord as one of our official events. Find more information about PyWeek on their official website.

Aug 16th, 2020
-
- + +
-
Picture -
+

Python Discord hits 100,000 members.

-

After years of hard work, we hit 100,000 users. A monumental milestone, and one we're very proud of. To commemorate it, we create this timeline.

+

After years of hard work, we hit 100,000 users. A monumental milestone, + and one we're very proud of. To commemorate it, we create this timeline.

Sep ??, 2020
-
- + + - + {% endblock %} -- cgit v1.2.3 From c0e9af6d8c736205bcefc92bf89e6d587db29455 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Mon, 12 Oct 2020 00:31:04 +0100 Subject: Remove flake8-annotations timeline item --- pydis_site/templates/home/timeline.html | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/pydis_site/templates/home/timeline.html b/pydis_site/templates/home/timeline.html index da89c25e..6e466106 100644 --- a/pydis_site/templates/home/timeline.html +++ b/pydis_site/templates/home/timeline.html @@ -256,21 +256,6 @@ -
-
- Picture -
- -
-

flake8-annotations is released on PyPI

-

lorem ipsum

- -
- Sep 25th, 2019 -
-
-
-
Picture -- cgit v1.2.3 From 059f6c07b755290634f9aae1a3657d3c4d761b43 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Mon, 12 Oct 2020 00:32:44 +0100 Subject: Add logo to new logo timeline item --- pydis_site/templates/home/timeline.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/templates/home/timeline.html b/pydis_site/templates/home/timeline.html index 6e466106..0c8fc61c 100644 --- a/pydis_site/templates/home/timeline.html +++ b/pydis_site/templates/home/timeline.html @@ -53,7 +53,7 @@

Our logo is born. Thanks @Aperture!

-

picture here?

+

Feb 3rd, 2018 -- cgit v1.2.3 From 410bfd8996af861beedacc6711adce26dc5f684b Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Mon, 12 Oct 2020 00:40:12 +0100 Subject: Responsively size YouTube embeds --- pydis_site/static/css/home/timeline.css | 14 +++++++++++++ pydis_site/templates/home/timeline.html | 36 +++++++++++++++++++++------------ 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/pydis_site/static/css/home/timeline.css b/pydis_site/static/css/home/timeline.css index f2920686..77b2f8da 100644 --- a/pydis_site/static/css/home/timeline.css +++ b/pydis_site/static/css/home/timeline.css @@ -29,6 +29,20 @@ button, input, textarea, select { margin: 0 } +.video-container { + position: relative; + width: 100%; + height: 0; + padding-bottom: 56.25%; +} +.video { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + .btn, .form-control, .link, .reset { background-color: transparent; padding: 0; diff --git a/pydis_site/templates/home/timeline.html b/pydis_site/templates/home/timeline.html index 0c8fc61c..9c7817e9 100644 --- a/pydis_site/templates/home/timeline.html +++ b/pydis_site/templates/home/timeline.html @@ -53,7 +53,9 @@

Our logo is born. Thanks @Aperture!

-

+

+

Feb 3rd, 2018 @@ -229,9 +231,11 @@

PyDis hits 15,000 members; the โ€œhot ones specialโ€ video is released

- +
+ +
Apr 8th, 2019 @@ -300,9 +304,11 @@ developers join us to judge the event and help out our members during the event. One of them, @tshirtman, even joins our staff!

- +
+ +
Jan 17, 2020 @@ -356,9 +362,11 @@ Several of the Code Jam participants also end up getting involved contributing to the Arcade repository.

- +
+ +
Apr 17th, 2020 @@ -427,9 +435,11 @@ Code Jam for 2020 attracts hundreds of participants, and sees the creation of some fantastic projects. Check them out in our judge stream below:

- +
+ +
Jul 31st, 2020 -- cgit v1.2.3 From 74992b465b7acceadaee551ed6aa3337a61213ac Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Mon, 12 Oct 2020 00:58:40 +0100 Subject: Add fontawesome icons to every timeline event --- pydis_site/templates/home/timeline.html | 46 ++++++++++++++++----------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/pydis_site/templates/home/timeline.html b/pydis_site/templates/home/timeline.html index 9c7817e9..fee54d66 100644 --- a/pydis_site/templates/home/timeline.html +++ b/pydis_site/templates/home/timeline.html @@ -31,7 +31,7 @@
- Picture +
@@ -65,7 +65,7 @@
- Picture +
@@ -83,7 +83,7 @@
- Picture +
@@ -102,7 +102,7 @@
- Picture +
@@ -121,7 +121,7 @@
- Picture +
@@ -140,7 +140,7 @@
- Picture +
@@ -157,7 +157,7 @@
- Picture +
@@ -174,7 +174,7 @@
- Picture +
@@ -192,7 +192,7 @@
- Picture +
@@ -208,7 +208,7 @@
- Picture +
@@ -226,7 +226,7 @@
- Picture +
@@ -245,7 +245,7 @@
- Picture +
@@ -262,7 +262,7 @@
- Picture +
@@ -279,7 +279,7 @@
- Picture +
@@ -295,7 +295,7 @@
- Picture +
@@ -318,7 +318,7 @@
- Picture +
@@ -353,7 +353,7 @@
- Picture +
@@ -376,7 +376,7 @@
- Picture +
@@ -393,7 +393,7 @@
- Picture +
@@ -410,7 +410,7 @@
- Picture +
@@ -426,7 +426,7 @@
- Picture +
@@ -449,7 +449,7 @@
- Picture +
@@ -466,7 +466,7 @@
- Picture +
-- cgit v1.2.3 From 914614e58e901e5f32b632b2cc05f151dafd71e8 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Mon, 12 Oct 2020 00:58:49 +0100 Subject: Better scale YouTube videos --- pydis_site/static/css/home/timeline.css | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pydis_site/static/css/home/timeline.css b/pydis_site/static/css/home/timeline.css index 77b2f8da..00e0b447 100644 --- a/pydis_site/static/css/home/timeline.css +++ b/pydis_site/static/css/home/timeline.css @@ -33,7 +33,7 @@ button, input, textarea, select { position: relative; width: 100%; height: 0; - padding-bottom: 56.25%; + padding-bottom: 75%; } .video { position: absolute; @@ -3463,9 +3463,14 @@ mark { box-shadow: 0 0 0 4px var(--color-white), inset 0 2px 0 rgba(0, 0, 0, 0.08), 0 3px 0 4px rgba(0, 0, 0, 0.05) } +.cd-timeline__img i { + font-size: 1.5em; + color: white; +} + .cd-timeline__img img { - width: 24px; - height: 24px + width: 30px; + height: 30px; } @media (min-width: 64rem) { @@ -3477,14 +3482,14 @@ mark { margin-left: calc(5% - 30px); will-change: transform } + .cd-timeline__block:nth-child(even) .cd-timeline__img { margin-right: calc(5% - 30px) } } .cd-timeline__img--picture { - background-color: hsl(111, 51%, 60%); - background-color: var(--cd-color-4) + background-color: #7289DA; } .cd-timeline__img--movie { -- cgit v1.2.3 From 1da259dde97b856262185fc06dd25d5bae385cff Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Mon, 12 Oct 2020 00:58:56 +0100 Subject: Update default picture to community logo --- .../static/images/timeline/cd-icon-picture.svg | 83 ++++++++++++++++++++-- 1 file changed, 78 insertions(+), 5 deletions(-) diff --git a/pydis_site/static/images/timeline/cd-icon-picture.svg b/pydis_site/static/images/timeline/cd-icon-picture.svg index dd5f180b..b173ff12 100755 --- a/pydis_site/static/images/timeline/cd-icon-picture.svg +++ b/pydis_site/static/images/timeline/cd-icon-picture.svg @@ -1,5 +1,78 @@ - - - + +image/svg+xml \ No newline at end of file -- cgit v1.2.3 From 479624eab5074427345ea46e226acd58807d285b Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Mon, 12 Oct 2020 01:03:42 +0100 Subject: Add unit test for timeline page --- pydis_site/apps/home/tests/test_views.py | 7 +++++++ pydis_site/apps/home/urls.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/pydis_site/apps/home/tests/test_views.py b/pydis_site/apps/home/tests/test_views.py index 572317a7..6ef4cf30 100644 --- a/pydis_site/apps/home/tests/test_views.py +++ b/pydis_site/apps/home/tests/test_views.py @@ -202,6 +202,13 @@ class TestIndexReturns200(TestCase): resp = self.client.get(url) self.assertEqual(resp.status_code, 200) +class TestTimelineReturns200(TestCase): + def test_timeline_returns_200(self): + """Check that the timeline page returns a HTTP 200 response.""" + url = reverse('timeline') + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + class TestLoginCancelledReturns302(TestCase): def test_login_cancelled_returns_302(self): diff --git a/pydis_site/apps/home/urls.py b/pydis_site/apps/home/urls.py index cf0c7457..14d118f8 100644 --- a/pydis_site/apps/home/urls.py +++ b/pydis_site/apps/home/urls.py @@ -38,5 +38,5 @@ urlpatterns = [ path('admin/', admin.site.urls), path('notifications/', include('django_nyt.urls')), - path('timeline/', timeline), + path('timeline/', timeline, name="timeline"), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) -- cgit v1.2.3 From aa1bd99d69a66ed2b931c864305b45664256cf69 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Mon, 12 Oct 2020 01:07:53 +0100 Subject: Fix linting in tests file --- pydis_site/apps/home/tests/test_views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pydis_site/apps/home/tests/test_views.py b/pydis_site/apps/home/tests/test_views.py index 6ef4cf30..40c80205 100644 --- a/pydis_site/apps/home/tests/test_views.py +++ b/pydis_site/apps/home/tests/test_views.py @@ -202,6 +202,7 @@ class TestIndexReturns200(TestCase): resp = self.client.get(url) self.assertEqual(resp.status_code, 200) + class TestTimelineReturns200(TestCase): def test_timeline_returns_200(self): """Check that the timeline page returns a HTTP 200 response.""" -- cgit v1.2.3 From 4cb6b5abd96e1aa83380d4fb990985edd4346260 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Mon, 12 Oct 2020 01:16:04 +0100 Subject: Update missed icon --- pydis_site/templates/home/timeline.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/templates/home/timeline.html b/pydis_site/templates/home/timeline.html index fee54d66..558257ac 100644 --- a/pydis_site/templates/home/timeline.html +++ b/pydis_site/templates/home/timeline.html @@ -336,7 +336,7 @@
- Picture +
-- cgit v1.2.3 From 59a29dc7c2a716ba84e51100d2db947291e1328f Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Mon, 12 Oct 2020 01:16:35 +0100 Subject: Fix line endings --- pydis_site/static/images/timeline/cd-icon-picture.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/static/images/timeline/cd-icon-picture.svg b/pydis_site/static/images/timeline/cd-icon-picture.svg index b173ff12..6ebd0d83 100755 --- a/pydis_site/static/images/timeline/cd-icon-picture.svg +++ b/pydis_site/static/images/timeline/cd-icon-picture.svg @@ -75,4 +75,4 @@ class="st3" id="path1950" inkscape:connector-curvature="0" - inkscape:label="lower_snake" /> \ No newline at end of file + inkscape:label="lower_snake" /> -- cgit v1.2.3 From 0ad74500786c7e3389c475aa6affbc9b6728c5bc Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Mon, 12 Oct 2020 01:22:35 +0100 Subject: Hyperlink stats page --- pydis_site/templates/home/timeline.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/templates/home/timeline.html b/pydis_site/templates/home/timeline.html index 558257ac..f785fde4 100644 --- a/pydis_site/templates/home/timeline.html +++ b/pydis_site/templates/home/timeline.html @@ -416,7 +416,7 @@

Python Discord Public Statistics are now live

After getting numerous requests to publish beautiful data on member - count and channel use, we create https://stats.pythondiscord.com/ for all to enjoy.

+ count and channel use, we create stats.pythondiscord.com for all to enjoy.

Jun 4th, 2020 -- cgit v1.2.3 From 31ce101536c92c69ff543a7fe64982368d90b9a3 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Mon, 12 Oct 2020 01:23:30 +0100 Subject: Update privacy policy icon --- pydis_site/templates/home/timeline.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/templates/home/timeline.html b/pydis_site/templates/home/timeline.html index f785fde4..ad81a703 100644 --- a/pydis_site/templates/home/timeline.html +++ b/pydis_site/templates/home/timeline.html @@ -102,7 +102,7 @@
- +
-- cgit v1.2.3 From 8a9337ecb120c325e7f2f4c22e2d4c1b46ac3f48 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Mon, 12 Oct 2020 01:30:23 +0100 Subject: Use bigger icon for timeline items --- .../static/images/timeline/cd-icon-picture.svg | 82 ++++++++++------------ 1 file changed, 38 insertions(+), 44 deletions(-) diff --git a/pydis_site/static/images/timeline/cd-icon-picture.svg b/pydis_site/static/images/timeline/cd-icon-picture.svg index 6ebd0d83..c2e297ac 100755 --- a/pydis_site/static/images/timeline/cd-icon-picture.svg +++ b/pydis_site/static/images/timeline/cd-icon-picture.svg @@ -7,16 +7,19 @@ xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - xml:space="preserve" - viewBox="0 0 463.86203 463.86203" - y="0px" - x="0px" - id="svg8" + enable-background="new 0 0 438 438" version="1.1" - sodipodi:docname="logo_discord.svg" + viewBox="0 0 346.16486 345.72064" + xml:space="preserve" + id="svg6016" + sodipodi:docname="logo_solo.svg" inkscape:version="0.91 r13725" - width="463.86203" - height="463.86203">image/svg+xml + style="fill:#ffffff" /> \ No newline at end of file -- cgit v1.2.3 From f3c28395e234b34006d986ae572c6f010796c4a9 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Mon, 12 Oct 2020 01:30:34 +0100 Subject: Use logo with text for new logo timeline itemm --- pydis_site/static/css/home/timeline.css | 9 +++++++++ pydis_site/templates/home/timeline.html | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/pydis_site/static/css/home/timeline.css b/pydis_site/static/css/home/timeline.css index 00e0b447..11080766 100644 --- a/pydis_site/static/css/home/timeline.css +++ b/pydis_site/static/css/home/timeline.css @@ -43,6 +43,15 @@ button, input, textarea, select { height: 100%; } +.pydis-logo-banner { + background-color: #7289DA !important; + border-radius: 10px; +} + +.pydis-logo-banner img { + padding-right: 20px; +} + .btn, .form-control, .link, .reset { background-color: transparent; padding: 0; diff --git a/pydis_site/templates/home/timeline.html b/pydis_site/templates/home/timeline.html index ad81a703..681f5be2 100644 --- a/pydis_site/templates/home/timeline.html +++ b/pydis_site/templates/home/timeline.html @@ -53,8 +53,8 @@

Our logo is born. Thanks @Aperture!

-

+

-- cgit v1.2.3 From d309d8e2218fa0ca60c68f0a5e757b36cc44f889 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Mon, 12 Oct 2020 01:44:45 +0100 Subject: Use different colours for each category --- pydis_site/static/css/home/timeline.css | 32 +++++++++++++++++++ pydis_site/templates/home/timeline.html | 54 ++++++++++++++++----------------- 2 files changed, 59 insertions(+), 27 deletions(-) diff --git a/pydis_site/static/css/home/timeline.css b/pydis_site/static/css/home/timeline.css index 11080766..62b4834f 100644 --- a/pydis_site/static/css/home/timeline.css +++ b/pydis_site/static/css/home/timeline.css @@ -29,6 +29,38 @@ button, input, textarea, select { margin: 0 } +.pastel-red { + background-color: #FF7878 !important; +} + +.pastel-orange { + background-color: #FFBF76 !important; +} + +.pastel-green { + background-color: #8bd6a7 !important; +} + +.pastel-blue { + background-color: #8edbec !important; +} + +.pastel-purple { + background-color: #CBB1FF !important; +} + +.pastel-pink { + background-color: #F6ACFF !important; +} + +.pastel-lime { + background-color: #c7f835 !important; +} + +.pastel-dark-blue { + background-color: #6982FF !important; +} + .video-container { position: relative; width: 100%; diff --git a/pydis_site/templates/home/timeline.html b/pydis_site/templates/home/timeline.html index 681f5be2..1459b432 100644 --- a/pydis_site/templates/home/timeline.html +++ b/pydis_site/templates/home/timeline.html @@ -30,7 +30,7 @@
-
+
@@ -64,7 +64,7 @@
-
+
@@ -82,7 +82,7 @@
-
+
@@ -101,7 +101,7 @@
-
+
@@ -120,7 +120,7 @@
-
+
@@ -139,7 +139,7 @@
-
+
@@ -156,7 +156,7 @@
-
+
@@ -173,7 +173,7 @@
-
+
@@ -191,7 +191,7 @@
-
+
@@ -207,8 +207,8 @@
-
- +
+
@@ -225,7 +225,7 @@
-
+
@@ -244,7 +244,7 @@
-
+
@@ -261,7 +261,7 @@
-
+
@@ -278,7 +278,7 @@
-
+
@@ -294,7 +294,7 @@
-
+
@@ -317,8 +317,8 @@
-
- +
+
@@ -335,7 +335,7 @@
-
+
@@ -352,7 +352,7 @@
-
+
@@ -375,8 +375,8 @@
-
- +
+
@@ -392,7 +392,7 @@
-
+
@@ -409,7 +409,7 @@
-
+
@@ -425,7 +425,7 @@
-
+
@@ -448,7 +448,7 @@
-
+
@@ -465,7 +465,7 @@
-
+
-- cgit v1.2.3 From 53e1a6465dba8252011cf743b630a52020a8e83b Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Mon, 12 Oct 2020 01:48:36 +0100 Subject: Add Ves joining timeline itemm --- pydis_site/static/css/home/timeline.css | 2 +- pydis_site/templates/home/timeline.html | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/pydis_site/static/css/home/timeline.css b/pydis_site/static/css/home/timeline.css index 62b4834f..13a6e0ca 100644 --- a/pydis_site/static/css/home/timeline.css +++ b/pydis_site/static/css/home/timeline.css @@ -54,7 +54,7 @@ button, input, textarea, select { } .pastel-lime { - background-color: #c7f835 !important; + background-color: #b6df3a !important; } .pastel-dark-blue { diff --git a/pydis_site/templates/home/timeline.html b/pydis_site/templates/home/timeline.html index 1459b432..5c71f3a7 100644 --- a/pydis_site/templates/home/timeline.html +++ b/pydis_site/templates/home/timeline.html @@ -277,6 +277,21 @@
+
+
+ Picture +
+ +
+

Ves Zappa becomes an owner

+

After being a long time active contributor to our projects and the driving force behind our events, Ves Zappa joined the Owners team alongside joe & lemon.

+ +
+ Sept 22nd, 2019 +
+
+
+
-- cgit v1.2.3 From e1cd1214ac54473634707fb558a889f89a917501 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Mon, 12 Oct 2020 01:49:15 +0100 Subject: Make dark blue darker to separate from blurple --- pydis_site/static/css/home/timeline.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/static/css/home/timeline.css b/pydis_site/static/css/home/timeline.css index 13a6e0ca..e7e0b264 100644 --- a/pydis_site/static/css/home/timeline.css +++ b/pydis_site/static/css/home/timeline.css @@ -58,7 +58,7 @@ button, input, textarea, select { } .pastel-dark-blue { - background-color: #6982FF !important; + background-color: #576297 !important; } .video-container { -- cgit v1.2.3 From 96860be9cbe6d3dba680e8ef9ac9db410bfd8e19 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Mon, 12 Oct 2020 01:49:54 +0100 Subject: Fix line endings --- pydis_site/static/images/timeline/cd-icon-picture.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/static/images/timeline/cd-icon-picture.svg b/pydis_site/static/images/timeline/cd-icon-picture.svg index c2e297ac..015718a8 100755 --- a/pydis_site/static/images/timeline/cd-icon-picture.svg +++ b/pydis_site/static/images/timeline/cd-icon-picture.svg @@ -69,4 +69,4 @@ id="path6014" inkscape:label="lower_snake" inkscape:connector-curvature="0" - style="fill:#ffffff" /> \ No newline at end of file + style="fill:#ffffff" /> -- cgit v1.2.3 From 924f54a83246fa0ec201daac0b391a2f59fe9aff Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Mon, 12 Oct 2020 09:52:11 +0100 Subject: Align server icons better --- pydis_site/static/css/home/timeline.css | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/pydis_site/static/css/home/timeline.css b/pydis_site/static/css/home/timeline.css index e7e0b264..fd364d38 100644 --- a/pydis_site/static/css/home/timeline.css +++ b/pydis_site/static/css/home/timeline.css @@ -3510,8 +3510,19 @@ mark { } .cd-timeline__img img { - width: 30px; - height: 30px; + width: 50px; + height: 50px; + margin-left: 2px; + margin-top: 2px; +} + +@media (max-width: 64rem) { + .cd-timeline__img img { + width: 30px; + height: 30px; + margin-left: 2px; + margin-top: 2px; + } } @media (min-width: 64rem) { -- cgit v1.2.3 From 26274d5e5ca61237158d35feb77e99f02f9495d2 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Mon, 12 Oct 2020 10:39:16 +0100 Subject: Update font sizing Co-authored-by: scragly <29337040+scragly@users.noreply.github.com> --- pydis_site/static/css/home/timeline.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/static/css/home/timeline.css b/pydis_site/static/css/home/timeline.css index fd364d38..73698c7c 100644 --- a/pydis_site/static/css/home/timeline.css +++ b/pydis_site/static/css/home/timeline.css @@ -3594,7 +3594,7 @@ mark { flex-grow: 0; will-change: transform; margin: 0; - font-size: 0.8em; + font-size: 0.9em; --line-height-multiplier: 1.2 } .cd-timeline__content::before { -- cgit v1.2.3 From 730efe6cff7b61610d97bc6d3401acf5617bd17b Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 18 Oct 2020 19:38:32 +0100 Subject: Exclude deleted messages from total message count --- pydis_site/apps/api/models/bot/metricity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py index 25b42fa2..cdfbb499 100644 --- a/pydis_site/apps/api/models/bot/metricity.py +++ b/pydis_site/apps/api/models/bot/metricity.py @@ -33,7 +33,7 @@ class Metricity: def total_messages(self, user_id: str) -> int: """Query total number of messages for a user.""" - self.cursor.execute("SELECT COUNT(*) FROM messages WHERE author_id = '%s'", [user_id]) + self.cursor.execute("SELECT COUNT(*) FROM messages WHERE author_id = '%s' AND NOT is_deleted", [user_id]) values = self.cursor.fetchone() if not values: -- cgit v1.2.3 From d0853d28b34789756d03f414dc33fa84bc774142 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 18 Oct 2020 19:42:39 +0100 Subject: Split query into multiple lines for total messages --- pydis_site/apps/api/models/bot/metricity.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py index cdfbb499..eed1deb4 100644 --- a/pydis_site/apps/api/models/bot/metricity.py +++ b/pydis_site/apps/api/models/bot/metricity.py @@ -33,7 +33,10 @@ class Metricity: def total_messages(self, user_id: str) -> int: """Query total number of messages for a user.""" - self.cursor.execute("SELECT COUNT(*) FROM messages WHERE author_id = '%s' AND NOT is_deleted", [user_id]) + self.cursor.execute( + "SELECT COUNT(*) FROM messages WHERE author_id = '%s' AND NOT is_deleted", + [user_id] + ) values = self.cursor.fetchone() if not values: -- cgit v1.2.3 From ecdd657f92feab725833a5a2a09bd8df6d0d6499 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 18 Oct 2020 21:05:56 +0100 Subject: Increase harakiri timeout --- docker/uwsgi.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/uwsgi.ini b/docker/uwsgi.ini index 3f35258c..643a0b42 100644 --- a/docker/uwsgi.ini +++ b/docker/uwsgi.ini @@ -23,7 +23,7 @@ procname-prefix-spaced = pydis_site : ### Worker options # Kill workers if they take more than 30 seconds to respond. -harakiri = 30 +harakiri = 300 ### Startup settings # Exit if we can't load the app -- cgit v1.2.3 From b53ac936d947ffed135bf5ebde50e043a56adbb2 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Mon, 19 Oct 2020 22:25:02 +1000 Subject: Allow requesting API data from internal DNS --- pydis_site/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pydis_site/settings.py b/pydis_site/settings.py index 1ae97b86..2b9f77fb 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -80,6 +80,7 @@ else: 'api.pydis.com', 'admin.pydis.com', 'staff.pydis.com', + 'api.site', ] ) SECRET_KEY = env('SECRET_KEY') -- cgit v1.2.3 From 2845c8f553a837c1aaaf1cd016036cfcc9d7be62 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Wed, 21 Oct 2020 23:03:15 +0100 Subject: Reset harakiri timeout --- docker/uwsgi.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/uwsgi.ini b/docker/uwsgi.ini index 643a0b42..3f35258c 100644 --- a/docker/uwsgi.ini +++ b/docker/uwsgi.ini @@ -23,7 +23,7 @@ procname-prefix-spaced = pydis_site : ### Worker options # Kill workers if they take more than 30 seconds to respond. -harakiri = 300 +harakiri = 30 ### Startup settings # Exit if we can't load the app -- cgit v1.2.3 From 2d7f789652216f9684c6cd7ee85854f0bfb1af0b Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Thu, 22 Oct 2020 00:49:18 +0100 Subject: Fix some icon sizing in timeline CSS --- pydis_site/static/css/home/timeline.css | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/pydis_site/static/css/home/timeline.css b/pydis_site/static/css/home/timeline.css index 73698c7c..07e17e15 100644 --- a/pydis_site/static/css/home/timeline.css +++ b/pydis_site/static/css/home/timeline.css @@ -3497,8 +3497,8 @@ mark { align-items: center; -ms-flex-negative: 0; flex-shrink: 0; - width: 40px; - height: 40px; + width: 30px; + height: 30px; border-radius: 50%; box-shadow: 0 0 0 4px hsl(0, 0%, 100%), inset 0 2px 0 rgba(0, 0, 0, 0.08), 0 3px 0 4px rgba(0, 0, 0, 0.05); box-shadow: 0 0 0 4px var(--color-white), inset 0 2px 0 rgba(0, 0, 0, 0.08), 0 3px 0 4px rgba(0, 0, 0, 0.05) @@ -3509,17 +3509,23 @@ mark { color: white; } +@media (max-width: 64rem) { + .cd-timeline__img i { + font-size: 0.9em; + } +} + .cd-timeline__img img { - width: 50px; - height: 50px; + width: 40px; + height: 40px; margin-left: 2px; margin-top: 2px; } @media (max-width: 64rem) { .cd-timeline__img img { - width: 30px; - height: 30px; + width: 20px; + height: 20px; margin-left: 2px; margin-top: 2px; } @@ -3532,7 +3538,7 @@ mark { -ms-flex-order: 1; order: 1; margin-left: calc(5% - 30px); - will-change: transform + will-change: transform; } .cd-timeline__block:nth-child(even) .cd-timeline__img { -- cgit v1.2.3 From fd0dbb28ab5e3c25332edc1f494a19f9d40ae79a Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Thu, 22 Oct 2020 00:49:33 +0100 Subject: Add Core Dev Q&A to timeline --- pydis_site/templates/home/timeline.html | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pydis_site/templates/home/timeline.html b/pydis_site/templates/home/timeline.html index 5c71f3a7..ce25b139 100644 --- a/pydis_site/templates/home/timeline.html +++ b/pydis_site/templates/home/timeline.html @@ -478,6 +478,25 @@
+ +
+
+ Picture +
+ +
+

Python Discord hosts the 2020 CPython Core Developer Q&A

+
+ +
+ +
+ Apr 8th, 2019 +
+
+
-- cgit v1.2.3 From ed38da95b500b78c7ac8bba222926587dc17e10b Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Thu, 22 Oct 2020 00:54:36 +0100 Subject: Fix trailing whitespace --- pydis_site/templates/home/timeline.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/templates/home/timeline.html b/pydis_site/templates/home/timeline.html index ce25b139..9a0cae30 100644 --- a/pydis_site/templates/home/timeline.html +++ b/pydis_site/templates/home/timeline.html @@ -478,7 +478,7 @@
- +
Picture -- cgit v1.2.3 From 9d453e516a287d9fff9e1de693ca756a40c8fe0d Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Thu, 22 Oct 2020 01:49:25 +0100 Subject: Update 100K Date --- pydis_site/templates/home/timeline.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/templates/home/timeline.html b/pydis_site/templates/home/timeline.html index 9a0cae30..07374085 100644 --- a/pydis_site/templates/home/timeline.html +++ b/pydis_site/templates/home/timeline.html @@ -509,7 +509,7 @@ and one we're very proud of. To commemorate it, we create this timeline.

- Sep ??, 2020 + Oct 22nd, 2020
-- cgit v1.2.3 From ea3ba42c0d3dccdcb2f326be083d75063cb7b8dc Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Thu, 22 Oct 2020 01:53:42 +0100 Subject: Update core dev q&a date --- pydis_site/templates/home/timeline.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/templates/home/timeline.html b/pydis_site/templates/home/timeline.html index 07374085..54cbdc1b 100644 --- a/pydis_site/templates/home/timeline.html +++ b/pydis_site/templates/home/timeline.html @@ -493,7 +493,7 @@
- Apr 8th, 2019 + Oct 21st, 2020
-- cgit v1.2.3 From e5355de93315c51f5de800fa6412f73f4aa2a4fc Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Thu, 22 Oct 2020 14:29:33 +1000 Subject: Add wave-style hero, 100K promo, timeline card. --- pydis_site/static/css/home/index.css | 127 +++++++++++++++++++++++++++---- pydis_site/static/images/events/100k.png | Bin 0 -> 210477 bytes pydis_site/templates/home/index.html | 58 +++++++++++++- 3 files changed, 169 insertions(+), 16 deletions(-) create mode 100644 pydis_site/static/images/events/100k.png diff --git a/pydis_site/static/css/home/index.css b/pydis_site/static/css/home/index.css index ba856a8e..67caf61c 100644 --- a/pydis_site/static/css/home/index.css +++ b/pydis_site/static/css/home/index.css @@ -70,18 +70,117 @@ span.repo-language-dot.javascript { padding-bottom: 3rem; } -@media screen and (min-width: 1088px) { - .video-container iframe { - height: calc(42vw * 0.5625); - max-height: 371px; - max-width: 660px; - } -} - -@media screen and (max-width: 1087px) { - .video-container iframe { - height: calc(92vw * 0.5625); - max-height: none; - max-width: none; - } +.welcome-video { + box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); + transition: all 0.3s cubic-bezier(.25,.8,.25,1); + border-radius: 10px; + margin-top: 1em; +} + +.welcome-video:hover { + box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22); +} + +.aspect-wrapper { + position: relative; + padding-bottom: 56.25%; +} +.aspect-content { + top: 0; + left: 0; + width: 100%; + height: 100%; + position: absolute; +} + +.hero-card img{ + border-radius: 10px; + box-shadow: 0 1px 6px rgba(0,0,0,0.16), 0 1px 6px rgba(0,0,0,0.23); + margin-top: 1em; +} + +.main-head { + position: relative; + background-color: #7289DA; + color: #fff; + height: 32vw; + max-height: 500px; + overflow-x: hidden; + width: 100%; + padding: 0; +} + +.bottom-wave { + background: url(https://svgur.com/i/QVH.svg) repeat-x; + position: absolute; + bottom: 0; + width: 6400px; + height: 26px; + animation: wave 50s cubic-bezier( 0.36, 0.45, 0.63, 0.53) infinite; + transform: translate3d(0, 0, 0); + z-index: 3; +} + +.above-waves { + z-index: 4; +} + +.wave { + background: url(https://svgur.com/i/QSd.svg) repeat-x; + position: absolute; + bottom: 0; + width: 6400px; + height: 198px; + animation: wave 65s cubic-bezier( 0.36, 0.45, 0.63, 0.53) infinite; + transform: translate3d(0, 0, 0); +} + +.wave:nth-of-type(2) { + animation: wave 60s cubic-bezier( 0.36, 0.45, 0.63, 0.53) -.125s infinite, swell 60s ease -1.25s infinite; + opacity: 0.5; + height: 178px; + animation-delay: -50s; +} + +@keyframes wave { + 0% { + margin-left: 0; + } + 100% { + margin-left: -1600px; + } +} + +.showcase { + margin: 0 1em; +} + +.mini-timeline { + height: 3px; + position: relative; + margin: 50px 0 50px 0; + background: linear-gradient(to right, #ffffff00, #666666ff, #ffffff00); + text-align: center; +} +.mtl-item { + display: inline-block; + vertical-align: middle; + width: 30px; + height: 30px; + border-radius: 50%; + position: relative; + top: -14px; + margin: 0 4% 0 4%; + background-color: #3EB2EF; + color: white; + font-size: 15px; + line-height: 33px; + border:none; + box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); + transition: all 0.3s cubic-bezier(.25,.8,.25,1); +} + +.mtl-item:hover { + box-shadow: 0 2px 5px rgba(0,0,0,0.16), 0 2px 5px rgba(0,0,0,0.23); + transform: scale(1.5); } diff --git a/pydis_site/static/images/events/100k.png b/pydis_site/static/images/events/100k.png new file mode 100644 index 00000000..ae024d77 Binary files /dev/null and b/pydis_site/static/images/events/100k.png differ diff --git a/pydis_site/templates/home/index.html b/pydis_site/templates/home/index.html index f31363a4..04ad4465 100644 --- a/pydis_site/templates/home/index.html +++ b/pydis_site/templates/home/index.html @@ -9,6 +9,35 @@ {% block content %} {% include "base/navbar.html" %} +
+
+
+
+
+ +
+
+ +
+ 100K members! +
+ +
+ +
+ +
+
+
+ +
{# Who are we? #} @@ -38,8 +67,33 @@
{# Right column container #} -
- +
+
+

+ New Timeline! +

+
+ + + + + + +
+

+ Start from our humble beginnings to discover the events that made our community what it is today. +

+ +
-- cgit v1.2.3 From 235abfd6db3f684bf4c87ef9f77015636fdca488 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Thu, 22 Oct 2020 15:01:46 +1000 Subject: Use local svgs for wave elements. --- pydis_site/static/css/home/index.css | 4 +- pydis_site/static/images/waves/wave_dark.svg | 73 +++++++++++++++++++++++++ pydis_site/static/images/waves/wave_white.svg | 77 +++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 pydis_site/static/images/waves/wave_dark.svg create mode 100644 pydis_site/static/images/waves/wave_white.svg diff --git a/pydis_site/static/css/home/index.css b/pydis_site/static/css/home/index.css index 67caf61c..9e36a51c 100644 --- a/pydis_site/static/css/home/index.css +++ b/pydis_site/static/css/home/index.css @@ -111,7 +111,7 @@ span.repo-language-dot.javascript { } .bottom-wave { - background: url(https://svgur.com/i/QVH.svg) repeat-x; + background: url(../../images/waves/wave_white.svg) repeat-x; position: absolute; bottom: 0; width: 6400px; @@ -126,7 +126,7 @@ span.repo-language-dot.javascript { } .wave { - background: url(https://svgur.com/i/QSd.svg) repeat-x; + background: url(../../images/waves/wave_dark.svg) repeat-x; position: absolute; bottom: 0; width: 6400px; diff --git a/pydis_site/static/images/waves/wave_dark.svg b/pydis_site/static/images/waves/wave_dark.svg new file mode 100644 index 00000000..35174c47 --- /dev/null +++ b/pydis_site/static/images/waves/wave_dark.svg @@ -0,0 +1,73 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/pydis_site/static/images/waves/wave_white.svg b/pydis_site/static/images/waves/wave_white.svg new file mode 100644 index 00000000..441dacff --- /dev/null +++ b/pydis_site/static/images/waves/wave_white.svg @@ -0,0 +1,77 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + -- cgit v1.2.3 From a971b36e079345fff3142a9cc663670f3b2a2ff8 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Thu, 22 Oct 2020 15:45:14 +1000 Subject: Add a message section to replace hero section for mobiles. --- pydis_site/static/css/home/index.css | 5 +++++ pydis_site/templates/home/index.html | 13 ++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/pydis_site/static/css/home/index.css b/pydis_site/static/css/home/index.css index 9e36a51c..18f2d1aa 100644 --- a/pydis_site/static/css/home/index.css +++ b/pydis_site/static/css/home/index.css @@ -104,6 +104,7 @@ span.repo-language-dot.javascript { background-color: #7289DA; color: #fff; height: 32vw; + min-height: 270px; max-height: 500px; overflow-x: hidden; width: 100%; @@ -184,3 +185,7 @@ span.repo-language-dot.javascript { box-shadow: 0 2px 5px rgba(0,0,0,0.16), 0 2px 5px rgba(0,0,0,0.23); transform: scale(1.5); } + +.message { + margin: 5px; +} diff --git a/pydis_site/templates/home/index.html b/pydis_site/templates/home/index.html index 04ad4465..fbdd942d 100644 --- a/pydis_site/templates/home/index.html +++ b/pydis_site/templates/home/index.html @@ -9,7 +9,18 @@ {% block content %} {% include "base/navbar.html" %} -
+
+
+

100K Member Milestone!

+
+
+ Thanks to all our members for helping us create this friendly and helpful community! +

+ As a nice treat, we've created a Timeline page for people + to discover the events that made our community what it is today. Be sure to check it out! +
+
+
-- cgit v1.2.3 From 6437abed32c35b5c3de365b7af2cedf9b4adea42 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Thu, 22 Oct 2020 15:50:58 +1000 Subject: Override notice bottom margin, use new class instead of bulma class. --- pydis_site/static/css/home/index.css | 3 ++- pydis_site/templates/home/index.html | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pydis_site/static/css/home/index.css b/pydis_site/static/css/home/index.css index 18f2d1aa..cb79a6e6 100644 --- a/pydis_site/static/css/home/index.css +++ b/pydis_site/static/css/home/index.css @@ -186,6 +186,7 @@ span.repo-language-dot.javascript { transform: scale(1.5); } -.message { +.notice { margin: 5px; + margin-bottom: -10px!important; } diff --git a/pydis_site/templates/home/index.html b/pydis_site/templates/home/index.html index fbdd942d..24806399 100644 --- a/pydis_site/templates/home/index.html +++ b/pydis_site/templates/home/index.html @@ -9,7 +9,7 @@ {% block content %} {% include "base/navbar.html" %} -
+

100K Member Milestone!

-- cgit v1.2.3 From 53a11f43225c2f8d7a28295339ab70c7ce2225f8 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 22 Oct 2020 15:49:13 +0300 Subject: Improve timeline page animation Instead of just leaving all elements like they are when bounce-in is done, when they leave from view, make them bounce out and hidden again to let them bounce-in again. --- pydis_site/static/css/home/timeline.css | 80 +++++++++++++++++++++++++++++++++ pydis_site/static/js/timeline/main.js | 37 ++++++++++++++- 2 files changed, 115 insertions(+), 2 deletions(-) diff --git a/pydis_site/static/css/home/timeline.css b/pydis_site/static/css/home/timeline.css index 07e17e15..c7137fde 100644 --- a/pydis_site/static/css/home/timeline.css +++ b/pydis_site/static/css/home/timeline.css @@ -3652,6 +3652,14 @@ mark { -webkit-animation-name: cd-bounce-2-inverse; animation-name: cd-bounce-2-inverse } + .cd-timeline__img--bounce-out { + -webkit-animation: cd-bounce-out-1 0.6s; + animation: cd-bounce-out-1 0.6s; + } + .cd-timeline__content--bounce-out { + -webkit-animation: cd-bounce-out-2 0.6s; + animation: cd-bounce-out-2 0.6s; + } } @-webkit-keyframes cd-bounce-1 { @@ -3755,3 +3763,75 @@ mark { transform: translateX(0) } } + +@-webkit-keyframes cd-bounce-out-1 { + 0% { + opacity: 1; + -webkit-transform: scale(1); + transform: scale(1) + } + + 60% { + -webkit-transform: scale(1.2); + transform: scale(1.2) + } + + 100% { + opacity: 0; + -webkit-transform: scale(0.5); + transform: scale(0.5) + } +} + +@keyframes cd-bounce-out-1 { + 0% { + opacity: 1; + -webkit-transform: scale(1); + transform: scale(1); + } + + 60% { + -webkit-transform: scale(1.2); + transform: scale(1.2); + } + + 100% { + opacity: 0; + -webkit-transform: scale(0.5); + transform: scale(0.5); + } +} + +@-webkit-keyframes cd-bounce-out-2 { + 0% { + opacity: 1; + -webkit-transform: translateX(0); + transform: translateX(0) + } + 60% { + -webkit-transform: translateX(20px); + transform: translateX(20px) + } + 100% { + opacity: 0; + -webkit-transform: translateX(-100px); + transform: translateX(-100px) + } +} + +@keyframes cd-bounce-out-2 { + 0% { + opacity: 1; + -webkit-transform: translateX(0); + transform: translateX(0) + } + 60% { + -webkit-transform: translateX(20px); + transform: translateX(20px) + } + 100% { + opacity: 0; + -webkit-transform: translateX(-100px); + transform: translateX(-100px) + } +} diff --git a/pydis_site/static/js/timeline/main.js b/pydis_site/static/js/timeline/main.js index a4bf4f31..2ff7df57 100644 --- a/pydis_site/static/js/timeline/main.js +++ b/pydis_site/static/js/timeline/main.js @@ -1,5 +1,5 @@ (function(){ - // Vertical Timeline - by CodyHouse.co + // Vertical Timeline - by CodyHouse.co (modified) function VerticalTimeline( element ) { this.element = element; this.blocks = this.element.getElementsByClassName("cd-timeline__block"); @@ -32,17 +32,36 @@ var self = this; for( var i = 0; i < this.blocks.length; i++) { (function(i){ - if( self.contents[i].classList.contains("cd-timeline__content--hidden") && self.blocks[i].getBoundingClientRect().top <= window.innerHeight*self.offset ) { + if((self.contents[i].classList.contains("cd-timeline__content--hidden") || self.contents[i].classList.contains("cd-timeline__content--bounce-out")) && self.blocks[i].getBoundingClientRect().top <= window.innerHeight*self.offset ) { // add bounce-in animation self.images[i].classList.add("cd-timeline__img--bounce-in"); self.contents[i].classList.add("cd-timeline__content--bounce-in"); self.images[i].classList.remove("cd-timeline__img--hidden"); self.contents[i].classList.remove("cd-timeline__content--hidden"); + self.images[i].classList.remove("cd-timeline__img--bounce-out"); + self.contents[i].classList.remove("cd-timeline__content--bounce-out"); } })(i); } }; + VerticalTimeline.prototype.hideBlocksScroll = function () { + if ( ! "classList" in document.documentElement ) { + return; + } + var self = this; + for( var i = 0; i < this.blocks.length; i++) { + (function(i){ + if(self.contents[i].classList.contains("cd-timeline__content--bounce-in") && self.blocks[i].getBoundingClientRect().top > window.innerHeight*self.offset ) { + self.images[i].classList.remove("cd-timeline__img--bounce-in"); + self.contents[i].classList.remove("cd-timeline__content--bounce-in"); + self.images[i].classList.add("cd-timeline__img--bounce-out"); + self.contents[i].classList.add("cd-timeline__content--bounce-out"); + } + })(i); + } + } + var verticalTimelines = document.getElementsByClassName("js-cd-timeline"), verticalTimelinesArray = [], scrolling = false; @@ -60,11 +79,25 @@ (!window.requestAnimationFrame) ? setTimeout(checkTimelineScroll, 250) : window.requestAnimationFrame(checkTimelineScroll); } }); + + function animationEnd(event) { + if (event.target.classList.contains("cd-timeline__img--bounce-out")) { + event.target.classList.add("cd-timeline__img--hidden"); + event.target.classList.remove("cd-timeline__img--bounce-out"); + } else if (event.target.classList.contains("cd-timeline__content--bounce-out")) { + event.target.classList.add("cd-timeline__content--hidden"); + event.target.classList.remove("cd-timeline__content--bounce-out"); + } + } + + window.addEventListener("animationend", animationEnd); + window.addEventListener("webkitAnimationEnd", animationEnd); } function checkTimelineScroll() { verticalTimelinesArray.forEach(function(timeline){ timeline.showBlocks(); + timeline.hideBlocksScroll(); }); scrolling = false; }; -- cgit v1.2.3 From 0d5be9824b36b0551562be7a751583c6d256d643 Mon Sep 17 00:00:00 2001 From: Dennis Pham Date: Thu, 22 Oct 2020 10:50:00 -0400 Subject: Update Python Discord badge to 100k members. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 616f2edc..2f122fc4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Python Discord: Site -[![Discord](https://img.shields.io/static/v1?label=Python%20Discord&logo=discord&message=%3E95k%20members&color=%237289DA&logoColor=white)](https://discord.gg/2B963hn) +[![Discord](https://img.shields.io/static/v1?label=Python%20Discord&logo=discord&message=%3E100k%20members&color=%237289DA&logoColor=white)](https://discord.gg/2B963hn) [![Build Status](https://dev.azure.com/python-discord/Python%20Discord/_apis/build/status/Site?branchName=master)](https://dev.azure.com/python-discord/Python%20Discord/_build/latest?definitionId=2&branchName=master) [![Tests](https://img.shields.io/azure-devops/tests/python-discord/Python%20Discord/2?compact_message)](https://dev.azure.com/python-discord/Python%20Discord/_apis/build/status/Site?branchName=master) [![Coverage](https://img.shields.io/azure-devops/coverage/python-discord/Python%20Discord/2/master)](https://dev.azure.com/python-discord/Python%20Discord/_apis/build/status/Site?branchName=master) -- cgit v1.2.3 From 325f978b5f9d94deef8988617eb16a6cd97f0cbe Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 23 Oct 2020 21:52:54 +1000 Subject: Change discord img-based link to normal nav item. --- pydis_site/static/css/base/base.css | 18 ++++++++++++------ pydis_site/templates/base/navbar.html | 11 +++-------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/pydis_site/static/css/base/base.css b/pydis_site/static/css/base/base.css index dc7c504d..42bbc8cf 100644 --- a/pydis_site/static/css/base/base.css +++ b/pydis_site/static/css/base/base.css @@ -30,16 +30,22 @@ div.card.has-equal-height { background-color: transparent; } -.navbar-item.has-left-margin-1 { - margin-left: 1rem; +.navbar-link:not(.is-arrowless) { + padding-left: 2rem; + padding-right: 3.5em; } -.navbar-item.has-left-margin-2 { - margin-left: 2rem; +.navbar-link:not(.is-arrowless)::after { + right: 2em; + margin-top: -0.42em; } -.navbar-item.has-left-margin-3 { - margin-left: 3rem; +/* Dropdown nav needs to be viewable at edge of canvas */ +@media screen and (min-width: 1024px) { + .navbar-dropdown { + left: unset; + right: 0; + } } #navbar-banner { diff --git a/pydis_site/templates/base/navbar.html b/pydis_site/templates/base/navbar.html index c2915025..9453c7b5 100644 --- a/pydis_site/templates/base/navbar.html +++ b/pydis_site/templates/base/navbar.html @@ -20,8 +20,8 @@ -- cgit v1.2.3 From 0debbceebc7cad5212bae56e0250e885896a3035 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 23 Oct 2020 21:56:36 +1000 Subject: Use sections, remove unused CSS, improve repo cards. --- pydis_site/static/css/base/base.css | 16 ++- pydis_site/static/css/home/index.css | 248 +++++++++++++++++++---------------- pydis_site/templates/home/index.html | 166 ++++++++++++++--------- 3 files changed, 255 insertions(+), 175 deletions(-) diff --git a/pydis_site/static/css/base/base.css b/pydis_site/static/css/base/base.css index 42bbc8cf..306a3def 100644 --- a/pydis_site/static/css/base/base.css +++ b/pydis_site/static/css/base/base.css @@ -12,7 +12,7 @@ main.site-content { flex: 1; } -div.card.has-equal-height { +.card.has-equal-height { height: 100%; display: flex; flex-direction: column; @@ -117,3 +117,17 @@ button.is-size-navbar-menu, a.is-size-navbar-menu { .codehilite-wrap { margin-bottom: 1em; } + +/* 16:9 aspect ratio fixing */ +.force-aspect-container { + position: relative; + padding-bottom: 56.25%; +} + +.force-aspect-content { + top: 0; + left: 0; + width: 100%; + height: 100%; + position: absolute; +} diff --git a/pydis_site/static/css/home/index.css b/pydis_site/static/css/home/index.css index cb79a6e6..58ca8888 100644 --- a/pydis_site/static/css/home/index.css +++ b/pydis_site/static/css/home/index.css @@ -1,146 +1,86 @@ -.discord-banner { - border-radius: 0.5rem; +h1 { + padding-bottom: 0.5em; } -.hero-image { - width: 20rem; - margin: auto; -} - -.hero-body { - padding-top: 1rem; - padding-bottom: 1rem; -} - -.section-sp img { - height: 5rem; - margin-right: 2rem; -} +/* Mobile-only notice banner */ -.video-container iframe, -.video-container object, -.video-container embed { - width: 100%; - height: calc(92vw * 0.5625); - margin: 8px auto auto auto; +#mobile-notice { + margin: 5px; + margin-bottom: -10px!important; } -div.card.github-card { - box-shadow: none; - border: #d1d5da 1px solid; - border-radius: 3px; -} +/* Wave hero */ -div.repo-headline { - font-size: 1.25rem; - margin-bottom: 8px; -} - -span.repo-language-dot { - border-radius: 50%; - height: 12px; - width: 12px; - top: 1px; - display: inline-block; +#wave-hero { position: relative; + background-color: #7289DA; + color: #fff; + height: 32vw; + min-height: 270px; + max-height: 500px; + overflow-x: hidden; + width: 100%; + padding: 0; } -span.repo-language-dot.python { - background-color: #3572A5; -} - -span.repo-language-dot.html { - background-color: #e34c26; -} - -span.repo-language-dot.css { - background-color: #563d7c; -} - -span.repo-language-dot.javascript { - background-color: #f1e05a; -} - -#repo-footer-item { - margin-left: 1.2rem; +#wave-hero .container { + z-index: 4; /* keep hero contents above wave animations */ } -#sponsors-hero { - padding-top: 2rem; - padding-bottom: 3rem; +@media screen and (min-width: 769px) and (max-width: 1023px) { + #wave-hero .columns { + margin: 0 1em 0 1em; /* Stop cards touching canvas edges in table-view */ + } } -.welcome-video { +#wave-hero iframe { box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); transition: all 0.3s cubic-bezier(.25,.8,.25,1); border-radius: 10px; margin-top: 1em; + border: none; } -.welcome-video:hover { +#wave-hero iframe:hover { box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22); } -.aspect-wrapper { - position: relative; - padding-bottom: 56.25%; -} -.aspect-content { - top: 0; - left: 0; - width: 100%; - height: 100%; - position: absolute; -} - -.hero-card img{ +#wave-hero-right img{ border-radius: 10px; box-shadow: 0 1px 6px rgba(0,0,0,0.16), 0 1px 6px rgba(0,0,0,0.23); margin-top: 1em; + text-align: right; } -.main-head { - position: relative; - background-color: #7289DA; - color: #fff; - height: 32vw; - min-height: 270px; - max-height: 500px; - overflow-x: hidden; - width: 100%; - padding: 0; -} - -.bottom-wave { - background: url(../../images/waves/wave_white.svg) repeat-x; +#wave-hero .wave { + background: url(../../images/waves/wave_dark.svg) repeat-x; position: absolute; bottom: 0; width: 6400px; - height: 26px; - animation: wave 50s cubic-bezier( 0.36, 0.45, 0.63, 0.53) infinite; - transform: translate3d(0, 0, 0); - z-index: 3; + animation-name: wave; + animation-timing-function: cubic-bezier(.36,.45,.63,.53); + animation-iteration-count: infinite; + transform: translate3d(0,0,0); /* Trigger 3D acceleration for smoother animation */ } -.above-waves { - z-index: 4; +#front-wave { + animation-duration: 60s; + animation-delay: -50s; + opacity: 0.5; + height: 178px; } -.wave { - background: url(../../images/waves/wave_dark.svg) repeat-x; - position: absolute; - bottom: 0; - width: 6400px; +#back-wave { + animation-duration: 65s; height: 198px; - animation: wave 65s cubic-bezier( 0.36, 0.45, 0.63, 0.53) infinite; - transform: translate3d(0, 0, 0); } -.wave:nth-of-type(2) { - animation: wave 60s cubic-bezier( 0.36, 0.45, 0.63, 0.53) -.125s infinite, swell 60s ease -1.25s infinite; - opacity: 0.5; - height: 178px; - animation-delay: -50s; +#bottom-wave { + animation-duration: 50s; + animation-delay: -10s; + background: url(../../images/waves/wave_white.svg) repeat-x !important; + height: 26px; + z-index: 3; } @keyframes wave { @@ -152,18 +92,21 @@ span.repo-language-dot.javascript { } } -.showcase { +/* Showcase */ + +#showcase { margin: 0 1em; } -.mini-timeline { +#showcase .mini-timeline { height: 3px; position: relative; margin: 50px 0 50px 0; background: linear-gradient(to right, #ffffff00, #666666ff, #ffffff00); text-align: center; } -.mtl-item { + +#showcase .mini-timeline i { display: inline-block; vertical-align: middle; width: 30px; @@ -181,12 +124,91 @@ span.repo-language-dot.javascript { transition: all 0.3s cubic-bezier(.25,.8,.25,1); } -.mtl-item:hover { +#showcase .mini-timeline i:hover { box-shadow: 0 2px 5px rgba(0,0,0,0.16), 0 2px 5px rgba(0,0,0,0.23); transform: scale(1.5); } -.notice { - margin: 5px; - margin-bottom: -10px!important; +/* Projects */ + +#projects { + padding-top: 0; +} + +#projects .card { + box-shadow: none; + border: #d1d5da 1px solid; + border-radius: 3px; + transition: all 0.2s cubic-bezier(.25,.8,.25,1); + height: 100%; + display: flex; + flex-direction: column; +} + +#projects .card:hover { + box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); +} + +#projects .card-header { + box-shadow: none; + font-size: 1.25rem; + padding: 1.5rem 1.5rem 0 1.5rem; +} + +#projects .card-header-icon { + font-size: 1.5rem; + padding: 0 1rem 0 0; +} + +#projects .card-header-title { + padding: 0; + color: #7289DA; +} + +#projects .card:hover .card-header-title { + color: #363636; +} + +#projects .card-content { + padding-top: 8px; + padding-bottom: 1rem; +} + +#projects .card-footer { + margin-top: auto; + border: none; +} + +#projects .card-footer-item { + border: none; +} + +#projects .card-footer-item i { + margin-right: 0.5rem; +} + +#projects .repo-language-dot { + border-radius: 50%; + height: 12px; + width: 12px; + top: -1px; + display: inline-block; + position: relative; +} + +#projects .repo-language-dot.python { background-color: #3572A5; } +#projects .repo-language-dot.html { background-color: #e34c26; } +#projects .repo-language-dot.css { background-color: #563d7c; } +#projects .repo-language-dot.javascript { background-color: #f1e05a; } + +/* Sponsors */ + +#sponsors .hero-body { + padding-top: 2rem; + padding-bottom: 3rem; +} + +#sponsors img { + height: 5rem; + margin-right: 2rem; } diff --git a/pydis_site/templates/home/index.html b/pydis_site/templates/home/index.html index 24806399..7d91feb4 100644 --- a/pydis_site/templates/home/index.html +++ b/pydis_site/templates/home/index.html @@ -9,7 +9,8 @@ {% block content %} {% include "base/navbar.html" %} -
+ +

100K Member Milestone!

@@ -19,42 +20,58 @@ As a nice treat, we've created a Timeline page for people to discover the events that made our community what it is today. Be sure to check it out!
- -
-
-
-
-
+
+ + +
+ +
+
+ + {# Embedded Welcome video #} +
+
-
- 100K members! + {# Right side content #} +
+ 100K members!
-
-
-
-
+ {# Animated wave elements #} + + +
-
- {# Who are we? #} -
+ +
+ +

Who are we?

-
+

@@ -77,70 +94,97 @@

- {# Right column container #} -
-
-

- New Timeline! -

+ {# Showcase box #} +
+
-
+ + +
+
+
+
- {# Projects #} + +
+
- {# Sponsors #} -
-
+ +
+

Sponsors -- cgit v1.2.3 From f1ee1f154494728f305381df19d9f293b8667805 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 23 Oct 2020 22:00:27 +1000 Subject: Fix video aspect in timeline, quote img src urls. After updating the frontpage and creating an fixed-aspect-ratio style pair, I found I could just reuse them in the timeline to fix the inconsistent responsive aspect ratio that was occurring there. Some img src values also were not quoted; while this can work fine, it can break if the url has spaces or certain characters and is inconsistent with the rest of the project. --- pydis_site/static/css/home/timeline.css | 14 -------------- pydis_site/templates/home/timeline.html | 24 ++++++++++++------------ 2 files changed, 12 insertions(+), 26 deletions(-) diff --git a/pydis_site/static/css/home/timeline.css b/pydis_site/static/css/home/timeline.css index 07e17e15..89de0887 100644 --- a/pydis_site/static/css/home/timeline.css +++ b/pydis_site/static/css/home/timeline.css @@ -61,20 +61,6 @@ button, input, textarea, select { background-color: #576297 !important; } -.video-container { - position: relative; - width: 100%; - height: 0; - padding-bottom: 75%; -} -.video { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; -} - .pydis-logo-banner { background-color: #7289DA !important; border-radius: 10px; diff --git a/pydis_site/templates/home/timeline.html b/pydis_site/templates/home/timeline.html index 54cbdc1b..f3c58fc2 100644 --- a/pydis_site/templates/home/timeline.html +++ b/pydis_site/templates/home/timeline.html @@ -14,7 +14,7 @@
- Picture + Picture
@@ -231,8 +231,8 @@

PyDis hits 15,000 members; the โ€œhot ones specialโ€ video is released

-
-
@@ -319,8 +319,8 @@ developers join us to judge the event and help out our members during the event. One of them, @tshirtman, even joins our staff!

-
-
@@ -377,8 +377,8 @@ Several of the Code Jam participants also end up getting involved contributing to the Arcade repository.

-
-
@@ -450,8 +450,8 @@ Code Jam for 2020 attracts hundreds of participants, and sees the creation of some fantastic projects. Check them out in our judge stream below:

-
-
@@ -481,13 +481,13 @@
- Picture + Picture

Python Discord hosts the 2020 CPython Core Developer Q&A

-
-
-- cgit v1.2.3 From 7395563a2114ad00e07d5e7caf7986e7e6de5e53 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sun, 25 Oct 2020 01:09:16 +1000 Subject: Change discord nav item to distinct CTA button. --- pydis_site/static/css/base/base.css | 53 +++++--- pydis_site/static/images/navbar/discord.svg | 143 +++++++++++++++++++++ .../static/images/navbar/navbar_discordjoin.svg | 81 ------------ pydis_site/templates/base/navbar.html | 11 +- 4 files changed, 185 insertions(+), 103 deletions(-) create mode 100644 pydis_site/static/images/navbar/discord.svg delete mode 100644 pydis_site/static/images/navbar/navbar_discordjoin.svg diff --git a/pydis_site/static/css/base/base.css b/pydis_site/static/css/base/base.css index 306a3def..3ac2c503 100644 --- a/pydis_site/static/css/base/base.css +++ b/pydis_site/static/css/base/base.css @@ -18,42 +18,55 @@ main.site-content { flex-direction: column; } -.navbar-item.is-fullsize { - padding: 0; +.navbar { + padding-right: 0.8em; } -.navbar-item.is-fullsize img { - max-height: 4.75rem; +.navbar-item .navbar-link { + padding-left: 1.5em; + padding-right: 2.5em; +} + +.navbar-link:not(.is-arrowless)::after { + right: 1.125em; + margin-top: -0.455em; } .navbar-item.has-no-highlight:hover { background-color: transparent; } -.navbar-link:not(.is-arrowless) { - padding-left: 2rem; - padding-right: 3.5em; +#navbar-banner { + background-color: transparent; } -.navbar-link:not(.is-arrowless)::after { - right: 2em; - margin-top: -0.42em; +#navbar-banner img { + max-height: 3rem; } -/* Dropdown nav needs to be viewable at edge of canvas */ -@media screen and (min-width: 1024px) { - .navbar-dropdown { - left: unset; - right: 0; - } +#discord-btn a { + color: transparent; + background-image: url(../../images/navbar/discord.svg); + background-size: 200%; + background-position: 100% 50%; + background-repeat: no-repeat; + padding-left: 2.5rem; + padding-right: 2.5rem; + background-color: #697ec4ff; + margin-left: 0.5rem; + transition: all 0.2s cubic-bezier(.25,.8,.25,1); + overflow: hidden; } -#navbar-banner { - background-color: transparent; +#discord-btn:hover a { + box-shadow: 0 1px 4px rgba(0,0,0,0.16), 0 1px 6px rgba(0,0,0,0.23); + /*transform: scale(1.03) translate3d(0,0,0);*/ + background-size: 200%; + background-position: 5% 50%; } -#navbar-banner img { - max-height: 3rem; +#discord-btn:hover { + background-color: transparent; } #django-logo { diff --git a/pydis_site/static/images/navbar/discord.svg b/pydis_site/static/images/navbar/discord.svg new file mode 100644 index 00000000..e3090ee7 --- /dev/null +++ b/pydis_site/static/images/navbar/discord.svg @@ -0,0 +1,143 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + JOIN US + + + + diff --git a/pydis_site/static/images/navbar/navbar_discordjoin.svg b/pydis_site/static/images/navbar/navbar_discordjoin.svg deleted file mode 100644 index 75e6b102..00000000 --- a/pydis_site/static/images/navbar/navbar_discordjoin.svg +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - - - diff --git a/pydis_site/templates/base/navbar.html b/pydis_site/templates/base/navbar.html index 9453c7b5..d8abf36d 100644 --- a/pydis_site/templates/base/navbar.html +++ b/pydis_site/templates/base/navbar.html @@ -20,8 +20,8 @@ + + {# Desktop Nav Discord #} +
+ Discord +
+
+
-- cgit v1.2.3 From fcf0067ea4f68003ed0e2954d645398df82a43e0 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sun, 25 Oct 2020 01:09:49 +1000 Subject: Use local img for welcome video thumbnail. --- pydis_site/static/images/frontpage/welcome.jpg | Bin 0 -> 51725 bytes pydis_site/templates/home/index.html | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 pydis_site/static/images/frontpage/welcome.jpg diff --git a/pydis_site/static/images/frontpage/welcome.jpg b/pydis_site/static/images/frontpage/welcome.jpg new file mode 100644 index 00000000..0eb8f672 Binary files /dev/null and b/pydis_site/static/images/frontpage/welcome.jpg differ diff --git a/pydis_site/templates/home/index.html b/pydis_site/templates/home/index.html index 7d91feb4..7f59231c 100644 --- a/pydis_site/templates/home/index.html +++ b/pydis_site/templates/home/index.html @@ -42,7 +42,7 @@ span{height:1.5em;text-align:center;font:68px/1.5 sans-serif;color:#FFFFFFEE;text-shadow:0 0 0.1em #00000020} - Welcome to Python Discord + Welcome to Python Discord โ–ถ " allow="autoplay; accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture" -- cgit v1.2.3 From 75dadc4a76f795fd13a2e87cb2f68c2758b0e7ad Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sun, 25 Oct 2020 01:40:07 +1000 Subject: Remove blurple peek on Discord CTA, convert text to obj. --- pydis_site/static/css/base/base.css | 2 +- pydis_site/static/images/navbar/discord.svg | 42 ++++++++++++++++++++++------- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/pydis_site/static/css/base/base.css b/pydis_site/static/css/base/base.css index 3ac2c503..b53ff5d4 100644 --- a/pydis_site/static/css/base/base.css +++ b/pydis_site/static/css/base/base.css @@ -62,7 +62,7 @@ main.site-content { box-shadow: 0 1px 4px rgba(0,0,0,0.16), 0 1px 6px rgba(0,0,0,0.23); /*transform: scale(1.03) translate3d(0,0,0);*/ background-size: 200%; - background-position: 5% 50%; + background-position: 1% 50%; } #discord-btn:hover { diff --git a/pydis_site/static/images/navbar/discord.svg b/pydis_site/static/images/navbar/discord.svg index e3090ee7..406e3836 100644 --- a/pydis_site/static/images/navbar/discord.svg +++ b/pydis_site/static/images/navbar/discord.svg @@ -13,7 +13,7 @@ version="1.1" id="svg8" inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)" - sodipodi:docname="navbar_discordjoin2.svg"> + sodipodi:docname="discord.svg"> image/svg+xml - + @@ -106,7 +106,7 @@ + transform="matrix(0.90000009,0,0,0.90000009,10.464254,9.7980333)"> @@ -128,14 +128,36 @@ d="m 107.16039,90.382629 -3.19204,3.19205 c -0.15408,0.15408 -0.40352,0.15408 -0.55746,0 l -0.37229,-0.37231 c -0.15368,-0.15369 -0.15408,-0.40277 -5.3e-4,-0.55681 l 2.52975,-2.54167 -2.52975,-2.54164 c -0.15329,-0.15408 -0.15309,-0.40312 5.3e-4,-0.55681 l 0.37229,-0.37228 c 0.15408,-0.15408 0.40353,-0.15408 0.55746,0 l 3.19204,3.19201 c 0.15408,0.15407 0.15408,0.40354 0,0.55746 z" inkscape:connector-curvature="0" style="fill:#7289da;fill-opacity:1;stroke-width:0.0164247" /> - JOIN US + style="font-style:normal;font-weight:normal;font-size:6.35px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect953-0);fill:#7289da;fill-opacity:1;stroke:none"> + + + + + + +