Просмотр исходного кода

Move settings from settings.py to database (#1241)

Rafał Pitoń 6 лет назад
Родитель
Сommit
fafa7d8750
119 измененных файлов с 2658 добавлено и 2121 удалено
  1. 0 28
      devproject/settings.py
  2. 4 1
      misago/admin/src/style/admin-conf.scss
  3. 1 3
      misago/categories/api.py
  4. 79 85
      misago/categories/tests/test_utils.py
  5. 5 5
      misago/categories/utils.py
  6. 1 3
      misago/categories/views.py
  7. 2 2
      misago/conf/admin/__init__.py
  8. 25 0
      misago/conf/admin/forms/captcha.py
  9. 105 0
      misago/conf/admin/forms/threads.py
  10. 88 1
      misago/conf/admin/forms/users.py
  11. 98 0
      misago/conf/admin/tests/test_blank_avatar_validation.py
  12. 2 2
      misago/conf/context_processors.py
  13. 2 119
      misago/conf/defaults.py
  14. 23 0
      misago/conf/migrations/0004_create_settings.py
  15. 7 0
      misago/conf/shortcuts.py
  16. 64 0
      misago/conftest.py
  17. 1 1
      misago/core/exceptionhandler.py
  18. 7 7
      misago/core/tests/test_mail.py
  19. 4 1
      misago/legal/admin/__init__.py
  20. 7 7
      misago/readtracker/categoriestracker.py
  21. 13 0
      misago/readtracker/cutoffdate.py
  22. 0 23
      misago/readtracker/dates.py
  23. 4 3
      misago/readtracker/management/commands/clearreadtracker.py
  24. 1 69
      misago/readtracker/migrations/0003_migrate_reads_to_posts.py
  25. 5 5
      misago/readtracker/poststracker.py
  26. 23 0
      misago/readtracker/tests/conftest.py
  27. 161 172
      misago/readtracker/tests/test_categoriestracker.py
  28. 0 64
      misago/readtracker/tests/test_clearreadtracker.py
  29. 52 0
      misago/readtracker/tests/test_clearreadtracker_command.py
  30. 41 0
      misago/readtracker/tests/test_cutoff_date.py
  31. 0 62
      misago/readtracker/tests/test_dates.py
  32. 66 58
      misago/readtracker/tests/test_poststracker.py
  33. 87 131
      misago/readtracker/tests/test_threadstracker.py
  34. 7 6
      misago/readtracker/threadstracker.py
  35. 1 1
      misago/static/misago/admin/index.css
  36. BIN
      misago/static/misago/img/attachment-403.png
  37. BIN
      misago/static/misago/img/attachment-404.png
  38. BIN
      misago/static/misago/img/blank-avatar.png
  39. BIN
      misago/static/misago/img/error-403.png
  40. BIN
      misago/static/misago/img/error-404.png
  41. 9 0
      misago/templates/misago/admin/conf/captcha_settings.html
  42. 47 4
      misago/templates/misago/admin/conf/threads_settings.html
  43. 53 1
      misago/templates/misago/admin/conf/users_settings.html
  44. 2 2
      misago/templates/misago/admin/dashboard/checks.html
  45. 7 7
      misago/templates/misago/navbar.html
  46. 4 1
      misago/threads/admin/__init__.py
  47. 5 4
      misago/threads/api/postendpoints/delete.py
  48. 6 1
      misago/threads/api/postendpoints/merge.py
  49. 6 1
      misago/threads/api/postendpoints/move.py
  50. 17 7
      misago/threads/api/postendpoints/patch_post.py
  51. 2 2
      misago/threads/api/postendpoints/read.py
  52. 17 17
      misago/threads/api/postingendpoint/attachments.py
  53. 10 9
      misago/threads/api/postingendpoint/floodprotection.py
  54. 5 1
      misago/threads/api/threadendpoints/delete.py
  55. 16 7
      misago/threads/api/threadendpoints/patch.py
  56. 5 5
      misago/threads/management/commands/clearattachments.py
  57. 6 6
      misago/threads/search.py
  58. 40 33
      misago/threads/serializers/moderation.py
  59. 8 8
      misago/threads/tests/test_anonymize_data.py
  60. 290 298
      misago/threads/tests/test_attachments_middleware.py
  61. 273 0
      misago/threads/tests/test_attachments_proxy.py
  62. 0 238
      misago/threads/tests/test_attachmentview.py
  63. 58 73
      misago/threads/tests/test_clearattachments.py
  64. 10 10
      misago/threads/tests/test_emailnotification_middleware.py
  65. 185 29
      misago/threads/tests/test_floodprotection_middleware.py
  66. 53 17
      misago/threads/tests/test_gotoviews.py
  67. 10 10
      misago/threads/tests/test_privatethread_start_api.py
  68. 4 2
      misago/threads/tests/test_thread_bulkpatch_api.py
  69. 2 2
      misago/threads/tests/test_thread_model.py
  70. 1 1
      misago/threads/tests/test_thread_postbulkdelete_api.py
  71. 4 2
      misago/threads/tests/test_thread_postbulkpatch_api.py
  72. 4 6
      misago/threads/tests/test_thread_postmerge_api.py
  73. 4 9
      misago/threads/tests/test_thread_postmove_api.py
  74. 4 6
      misago/threads/tests/test_thread_postsplit_api.py
  75. 4 6
      misago/threads/tests/test_threads_bulkdelete_api.py
  76. 4 6
      misago/threads/tests/test_threads_merge_api.py
  77. 10 10
      misago/threads/tests/test_threadslists.py
  78. 8 9
      misago/threads/tests/test_threadview.py
  79. 4 5
      misago/threads/viewmodels/posts.py
  80. 1 1
      misago/threads/viewmodels/thread.py
  81. 8 9
      misago/threads/viewmodels/threads.py
  82. 8 7
      misago/threads/views/attachment.py
  83. 5 4
      misago/threads/views/goto.py
  84. 5 3
      misago/users/activepostersranking.py
  85. 1 1
      misago/users/admin/forms.py
  86. 4 1
      misago/users/admin/tasks.py
  87. 1 1
      misago/users/admin/views/users.py
  88. 5 4
      misago/users/api/users.py
  89. 20 14
      misago/users/apps.py
  90. 3 5
      misago/users/datadownloads/__init__.py
  91. 2 4
      misago/users/management/commands/createsuperuser.py
  92. 7 6
      misago/users/management/commands/deleteinactiveusers.py
  93. 4 2
      misago/users/management/commands/deletemarkedusers.py
  94. 4 6
      misago/users/management/commands/prepareuserdatadownloads.py
  95. 6 5
      misago/users/management/commands/removeoldips.py
  96. 7 3
      misago/users/models/user.py
  97. 3 4
      misago/users/permissions/delete.py
  98. 1 1
      misago/users/serializers/options.py
  99. 4 5
      misago/users/signals.py
  100. 0 82
      misago/users/tests/test_activepostersranking.py
  101. 91 157
      misago/users/tests/test_audittrail.py
  102. 11 1
      misago/users/tests/test_datadownloads.py
  103. 14 13
      misago/users/tests/test_deleteinactiveusers.py
  104. 3 2
      misago/users/tests/test_deletemarkedusers.py
  105. 2 2
      misago/users/tests/test_prepareuserdatadownloads.py
  106. 6 6
      misago/users/tests/test_remove_old_ips_command.py
  107. 1 1
      misago/users/tests/test_search.py
  108. 98 0
      misago/users/tests/test_stop_forum_spam_validator.py
  109. 77 0
      misago/users/tests/test_top_posters_ranking.py
  110. 10 10
      misago/users/tests/test_user_changeemail_api.py
  111. 11 11
      misago/users/tests/test_user_changepassword_api.py
  112. 7 7
      misago/users/tests/test_user_model.py
  113. 3 4
      misago/users/tests/test_user_requestdatadownload_api.py
  114. 6 2
      misago/users/tests/test_users_api.py
  115. 22 15
      misago/users/validators.py
  116. 1 2
      misago/users/viewmodels/activeposters.py
  117. 6 2
      misago/users/viewmodels/followers.py
  118. 6 2
      misago/users/viewmodels/rankusers.py
  119. 1 2
      misago/users/viewmodels/threads.py

+ 0 - 28
devproject/settings.py

@@ -359,46 +359,18 @@ MISAGO_AVATARS_SIZES = [400, 200, 100]
 MISAGO_SEARCH_CONFIG = "simple"
 
 
-# Allow users to download their personal data
-# Enables users to learn what data about them is being held by the site without having
-# to contact site's administrators.
-
-MISAGO_ENABLE_DOWNLOAD_OWN_DATA = True
-
 # Path to the directory that Misago should use to prepare user data downloads.
 # Should not be accessible from internet.
 
 MISAGO_USER_DATA_DOWNLOADS_WORKING_DIR = os.path.join(BASE_DIR, "userdata")
 
 
-# Allow users to delete their accounts
-# Lets users delete their own account on the site without having to contact site administrators.
-# This mechanism doesn't delete user posts, polls or attachments, but attempts to anonymize any
-# data about user left behind after user is deleted.
-
-MISAGO_ENABLE_DELETE_OWN_ACCOUNT = True
-
-
-# Automatically delete new user accounts that weren't activated in specified time
-# If you rely on admin review of new registrations, make this period long, disable
-# the "deleteinactiveusers" management command, or change this value to zero. Otherwise
-# keep it short to give users a chance to retry on their own after few days pass.
-
-MISAGO_DELETE_NEW_INACTIVE_USERS_OLDER_THAN_DAYS = 2
-
-
 # Path to directory containing avatar galleries
 # Those galleries can be loaded by running loadavatargallery command
 
 MISAGO_AVATAR_GALLERY = os.path.join(BASE_DIR, "avatargallery")
 
 
-# Specifies the number of days that IP addresses are stored in the database before removing.
-# Change this setting to None to never remove old IP addresses.
-
-MISAGO_IP_STORE_TIME = 50
-
-
 # Profile fields
 
 MISAGO_PROFILE_FIELDS = [

+ 4 - 1
misago/admin/src/style/admin-conf.scss

@@ -16,6 +16,8 @@
   @extend .p-2;
   @extend .rounded;
 
+  line-height: $line-height-sm;
+
   color: $body-color;
 
   &:hover,
@@ -28,7 +30,8 @@
 
 // Reset settings group name size
 .card-admin-settings-card h5 {
-  @extend .m-0;
+  @extend .mb-1;
 
   font-size: $font-size-base;
+  line-height: $line-height-sm;
 }

+ 1 - 3
misago/categories/api.py

@@ -7,7 +7,5 @@ from .utils import get_categories_tree
 
 class CategoryViewSet(viewsets.ViewSet):
     def list(self, request):
-        categories_tree = get_categories_tree(
-            request.user, request.user_acl, join_posters=True
-        )
+        categories_tree = get_categories_tree(request, join_posters=True)
         return Response(CategorySerializer(categories_tree, many=True).data)

+ 79 - 85
misago/categories/tests/test_utils.py

@@ -1,14 +1,55 @@
-from ...acl.useracl import get_user_acl
-from ...conftest import get_cache_versions
-from ...users.test import AuthenticatedUserTestCase
+from unittest.mock import Mock
+
+import pytest
+
 from ..models import Category
 from ..utils import get_categories_tree, get_category_path
 
-cache_versions = get_cache_versions()
-
 
-def get_patched_user_acl(user):
-    user_acl = get_user_acl(user, cache_versions)
+@pytest.fixture
+def categories_tree(root_category, default_category):
+    Category(name="Category A", slug="category-a").insert_at(
+        root_category, position="last-child", save=True
+    )
+    Category(name="Category E", slug="category-e").insert_at(
+        root_category, position="last-child", save=True
+    )
+
+    category_a = Category.objects.get(slug="category-a")
+
+    Category(name="Category B", slug="category-b").insert_at(
+        category_a, position="last-child", save=True
+    )
+
+    category_b = Category.objects.get(slug="category-b")
+
+    category_c = Category(name="Subcategory C", slug="subcategory-c").insert_at(
+        category_b, position="last-child", save=True
+    )
+    category_d = Category(name="Subcategory D", slug="subcategory-d").insert_at(
+        category_b, position="last-child", save=True
+    )
+
+    category_e = Category.objects.get(slug="category-e")
+    Category(name="Subcategory F", slug="subcategory-f").insert_at(
+        category_e, position="last-child", save=True
+    )
+
+    return {
+        "root": root_category,
+        "first": default_category,
+        "a": Category.objects.get(slug="category-a"),
+        "b": Category.objects.get(slug="category-b"),
+        "c": Category.objects.get(slug="subcategory-c"),
+        "d": Category.objects.get(slug="subcategory-d"),
+        "e": Category.objects.get(slug="category-e"),
+        "f": Category.objects.get(slug="subcategory-f"),
+    }
+
+
+@pytest.fixture
+def full_access_user_acl(categories_tree, user_acl):
+    user_acl = user_acl.copy()
     categories_acl = {"categories": {}, "visible_categories": []}
     for category in Category.objects.all_categories():
         categories_acl["visible_categories"].append(category.id)
@@ -17,81 +58,34 @@ def get_patched_user_acl(user):
     return user_acl
 
 
-class CategoriesUtilsTests(AuthenticatedUserTestCase):
-    def setUp(self):
-        """
-        Create categories tree for test cases:
-
-        First category (created by migration)
-
-        Category A
-          + Category B
-            + Subcategory C
-            + Subcategory D
-
-        Category E
-          + Subcategory F
-        """
-        super().setUp()
-
-        self.root = Category.objects.root_category()
-        self.first_category = Category.objects.get(slug="first-category")
-
-        Category(name="Category A", slug="category-a").insert_at(
-            self.root, position="last-child", save=True
-        )
-        Category(name="Category E", slug="category-e").insert_at(
-            self.root, position="last-child", save=True
-        )
-
-        self.category_a = Category.objects.get(slug="category-a")
-
-        Category(name="Category B", slug="category-b").insert_at(
-            self.category_a, position="last-child", save=True
-        )
-
-        self.category_b = Category.objects.get(slug="category-b")
-
-        Category(name="Subcategory C", slug="subcategory-c").insert_at(
-            self.category_b, position="last-child", save=True
-        )
-        Category(name="Subcategory D", slug="subcategory-d").insert_at(
-            self.category_b, position="last-child", save=True
-        )
-
-        self.category_e = Category.objects.get(slug="category-e")
-        Category(name="Subcategory F", slug="subcategory-f").insert_at(
-            self.category_e, position="last-child", save=True
-        )
-
-        self.user_acl = get_patched_user_acl(self.user)
-
-    def test_root_categories_tree_no_parent(self):
-        """get_categories_tree returns all children of root nodes"""
-        categories_tree = get_categories_tree(self.user, self.user_acl)
-        self.assertEqual(len(categories_tree), 3)
-
-        self.assertEqual(
-            categories_tree[0], Category.objects.get(slug="first-category")
-        )
-        self.assertEqual(categories_tree[1], Category.objects.get(slug="category-a"))
-        self.assertEqual(categories_tree[2], Category.objects.get(slug="category-e"))
-
-    def test_root_categories_tree_with_parent(self):
-        """get_categories_tree returns all children of given node"""
-        categories_tree = get_categories_tree(self.user, self.user_acl, self.category_a)
-        self.assertEqual(len(categories_tree), 1)
-        self.assertEqual(categories_tree[0], Category.objects.get(slug="category-b"))
-
-    def test_root_categories_tree_with_leaf(self):
-        """get_categories_tree returns all children of given node"""
-        categories_tree = get_categories_tree(
-            self.user, self.user_acl, Category.objects.get(slug="subcategory-f")
-        )
-        self.assertEqual(len(categories_tree), 0)
-
-    def test_get_category_path(self):
-        """get_categories_tree returns all children of root nodes"""
-        for node in get_categories_tree(self.user, self.user_acl):
-            parent_nodes = len(get_category_path(node))
-            self.assertEqual(parent_nodes, node.level)
+@pytest.fixture
+def request_mock(user, full_access_user_acl, dynamic_settings):
+    return Mock(settings=dynamic_settings, user=user, user_acl=full_access_user_acl)
+
+
+def test_tree_getter_defaults_to_returning_top_level_categories(
+    request_mock, categories_tree
+):
+    assert get_categories_tree(request_mock) == [
+        categories_tree["first"],
+        categories_tree["a"],
+        categories_tree["e"],
+    ]
+
+
+def test_tree_getter_returns_category_subtree(request_mock, categories_tree):
+    assert get_categories_tree(request_mock, categories_tree["a"]) == [
+        categories_tree["b"]
+    ]
+
+
+def test_tree_getter_returns_empty_list_for_leaf_category(
+    request_mock, categories_tree
+):
+    assert get_categories_tree(request_mock, categories_tree["f"]) == []
+
+
+def test_path_getter_returns_path_to_category(request_mock, categories_tree):
+    for node in get_categories_tree(request_mock):
+        parent_nodes = len(get_category_path(node))
+        assert parent_nodes == node.level

+ 5 - 5
misago/categories/utils.py

@@ -4,9 +4,9 @@ from .models import Category
 
 
 def get_categories_tree(
-    user, user_acl, parent=None, join_posters=False
+    request, parent=None, join_posters=False
 ):  # pylint: disable=too-many-branches
-    if not user_acl["visible_categories"]:
+    if not request.user_acl["visible_categories"]:
         return []
 
     if parent:
@@ -14,7 +14,7 @@ def get_categories_tree(
     else:
         queryset = Category.objects.all_categories()
 
-    queryset_with_acl = queryset.filter(id__in=user_acl["visible_categories"])
+    queryset_with_acl = queryset.filter(id__in=request.user_acl["visible_categories"])
     if join_posters:
         queryset_with_acl = queryset_with_acl.select_related("last_poster")
 
@@ -33,8 +33,8 @@ def get_categories_tree(
         if category.parent_id and category.level > parent_level:
             categories_dict[category.parent_id].subcategories.append(category)
 
-    add_acl_to_obj(user_acl, categories_list)
-    categoriestracker.make_read_aware(user, user_acl, categories_list)
+    add_acl_to_obj(request.user_acl, categories_list)
+    categoriestracker.make_read_aware(request, categories_list)
 
     for category in reversed(visible_categories):
         if category.acl["can_browse"]:

+ 1 - 3
misago/categories/views.py

@@ -6,9 +6,7 @@ from .utils import get_categories_tree
 
 
 def categories(request):
-    categories_tree = get_categories_tree(
-        request.user, request.user_acl, join_posters=True
-    )
+    categories_tree = get_categories_tree(request, join_posters=True)
 
     request.frontend_context.update(
         {

+ 2 - 2
misago/conf/admin/__init__.py

@@ -53,7 +53,7 @@ class MisagoAdminExtension:
         site.add_node(
             name=_("Users"),
             description=_(
-                "Customize user accounts default behaviour and features availability."
+                "Customize user accounts default behavior and features availability."
             ),
             parent="settings",
             namespace="users",
@@ -77,7 +77,7 @@ class MisagoAdminExtension:
         )
         site.add_node(
             name=_("Threads"),
-            description=_("Those settings control threads and posts."),
+            description=_("Threads, posts, polls and attachments options."),
             parent="settings",
             namespace="threads",
             after="analytics:index",

+ 25 - 0
misago/conf/admin/forms/captcha.py

@@ -1,6 +1,7 @@
 from django import forms
 from django.utils.translation import gettext_lazy as _
 
+from ....admin.forms import YesNoSwitch
 from .base import ChangeSettingsForm
 
 
@@ -12,6 +13,8 @@ class ChangeCaptchaSettingsForm(ChangeSettingsForm):
         "qa_question",
         "qa_help_text",
         "qa_answers",
+        "enable_stop_forum_spam",
+        "stop_forum_spam_confidence",
     ]
 
     captcha_type = forms.ChoiceField(
@@ -23,12 +26,14 @@ class ChangeCaptchaSettingsForm(ChangeSettingsForm):
         ],
         widget=forms.RadioSelect(),
     )
+
     recaptcha_site_key = forms.CharField(
         label=_("Site key"), max_length=100, required=False
     )
     recaptcha_secret_key = forms.CharField(
         label=_("Secret key"), max_length=100, required=False
     )
+
     qa_question = forms.CharField(
         label=_("Test question"), max_length=100, required=False
     )
@@ -43,6 +48,26 @@ class ChangeCaptchaSettingsForm(ChangeSettingsForm):
         required=False,
     )
 
+    enable_stop_forum_spam = YesNoSwitch(
+        label=_("Validate new registrations against SFS database"),
+        help_text=_(
+            "Turning this option on will result in Misago validating new user's e-mail "
+            "and IP address against SFS database."
+        ),
+    )
+    stop_forum_spam_confidence = forms.IntegerField(
+        label=_("Minimum SFS confidence required"),
+        help_text=_(
+            "SFS compares user e-mail and IP address with database of known spammers "
+            "and assigns the confidence score in range of 0 to 100 that user is a "
+            "spammer themselves. If this score is equal or higher than specified, "
+            "Misago will block user from registering and ban their IP address "
+            "for 24 hours."
+        ),
+        min_value=0,
+        max_value=100,
+    )
+
     def clean(self):
         cleaned_data = super().clean()
 

+ 105 - 0
misago/conf/admin/forms/threads.py

@@ -6,12 +6,44 @@ from .base import ChangeSettingsForm
 
 class ChangeThreadsSettingsForm(ChangeSettingsForm):
     settings = [
+        "attachment_403_image",
+        "attachment_404_image",
+        "daily_post_limit",
+        "hourly_post_limit",
+        "post_attachments_limit",
         "post_length_max",
         "post_length_min",
+        "readtracker_cutoff",
         "thread_title_length_max",
         "thread_title_length_min",
+        "unused_attachments_lifetime",
+        "threads_per_page",
+        "posts_per_page",
+        "posts_per_page_orphans",
+        "events_per_page",
     ]
 
+    daily_post_limit = forms.IntegerField(
+        label=_("Daily post limit per user"),
+        help_text=_(
+            "Daily limit of posts that may be posted by single user. "
+            "Fail-safe for situations when forum is flooded by spam bots. "
+            "Change to 0 to remove the limit."
+        ),
+        min_value=0,
+    )
+    hourly_post_limit = forms.IntegerField(
+        label=_("Hourly post limit per user"),
+        help_text=_(
+            "Hourly limit of posts that may be posted by single user. "
+            "Fail-safe for situations when forum is flooded by spam bots. "
+            "Change to 0 to remove the limit."
+        ),
+        min_value=0,
+    )
+    post_attachments_limit = forms.IntegerField(
+        label=_("Maximum number of attachments per post"), min_value=1
+    )
     post_length_max = forms.IntegerField(
         label=_("Maximum allowed post length"), min_value=0
     )
@@ -24,3 +56,76 @@ class ChangeThreadsSettingsForm(ChangeSettingsForm):
     thread_title_length_min = forms.IntegerField(
         label=_("Minimum required thread title length"), min_value=2, max_value=255
     )
+    unused_attachments_lifetime = forms.IntegerField(
+        label=_("Unused attachments lifetime"),
+        help_text=_(
+            "Period of time (in hours) after which user-uploaded files that weren't "
+            "attached to any post are deleted from disk."
+        ),
+        min_value=1,
+    )
+
+    readtracker_cutoff = forms.IntegerField(
+        label=_("Read-tracker cutoff"),
+        help_text=_(
+            "Controls amount of data used by read-tracking system. All content older "
+            "than number of days specified in this setting is considered old and read, "
+            "even if the opposite is true for the user. Active forums can try lowering "
+            "this value while less active ones may wish to increase it instead. "
+        ),
+        min_value=1,
+    )
+
+    threads_per_page = forms.IntegerField(
+        label=_("Number of threads displayed on a single page"), min_value=10
+    )
+
+    posts_per_page = forms.IntegerField(
+        label=_("Number of posts displayed on a single page"), min_value=5
+    )
+    posts_per_page_orphans = forms.IntegerField(
+        label=_("Maximum orphans"),
+        help_text=_(
+            "If number of posts to be displayed on the last page is less or equal to "
+            "number specified in this setting, those posts will instead be displayed "
+            "on previous page, reducing the total number of pages in thread."
+        ),
+        min_value=0,
+    )
+    events_per_page = forms.IntegerField(
+        label=_("Maximum number of events displayed on a single page"), min_value=5
+    )
+
+    attachment_403_image = forms.ImageField(
+        label=_("Permission denied"),
+        help_text=_(
+            "Attachments proxy will display this image in place of default one "
+            "when user tries to access attachment they have no permission to see."
+        ),
+        required=False,
+    )
+    attachment_403_image_delete = forms.BooleanField(
+        label=_("Delete custom permission denied image"), required=False
+    )
+    attachment_404_image = forms.ImageField(
+        label=_("Not found"),
+        help_text=_(
+            "Attachments proxy will display this image in place of default one "
+            "when user tries to access attachment that doesn't exist."
+        ),
+        required=False,
+    )
+    attachment_404_image_delete = forms.BooleanField(
+        label=_("Delete custom not found image"), required=False
+    )
+
+    def clean(self):
+        cleaned_data = super().clean()
+        if cleaned_data.get("posts_per_page_orphans") > cleaned_data.get(
+            "posts_per_page"
+        ):
+            self.add_error(
+                "posts_per_page_orphans",
+                _("This value must be lower than number of posts per page."),
+            )
+        return cleaned_data

+ 88 - 1
misago/conf/admin/forms/users.py

@@ -2,6 +2,7 @@ from django import forms
 from django.utils.translation import gettext_lazy as _
 
 from ....admin.forms import YesNoSwitch
+from ....users.validators import validate_username_content
 from ... import settings
 from .base import ChangeSettingsForm
 
@@ -19,6 +20,16 @@ class ChangeUsersSettingsForm(ChangeSettingsForm):
         "subscribe_start",
         "username_length_max",
         "username_length_min",
+        "anonymous_username",
+        "users_per_page",
+        "users_per_page_orphans",
+        "top_posters_ranking_length",
+        "top_posters_ranking_size",
+        "allow_data_downloads",
+        "data_downloads_expiration",
+        "allow_delete_own_account",
+        "new_inactive_accounts_delete",
+        "ip_storage_time",
     ]
 
     account_activation = forms.ChoiceField(
@@ -31,6 +42,15 @@ class ChangeUsersSettingsForm(ChangeSettingsForm):
         ],
         widget=forms.RadioSelect(),
     )
+    new_inactive_accounts_delete = forms.IntegerField(
+        label=_(
+            "Delete new inactive accounts if they weren't activated "
+            "within this number of days"
+        ),
+        help_text=_("Enter 0 to never delete inactive new accounts."),
+        min_value=0,
+    )
+
     username_length_min = forms.IntegerField(
         label=_("Minimum allowed username length"), min_value=2, max_value=20
     )
@@ -76,7 +96,7 @@ class ChangeUsersSettingsForm(ChangeSettingsForm):
         help_text=_(
             "Blank avatar is displayed in the interface when user's avatar is not "
             "available: when user was deleted or is guest. Uploaded image should be "
-            "a square"
+            "a square."
         ),
         required=False,
     )
@@ -113,6 +133,62 @@ class ChangeUsersSettingsForm(ChangeSettingsForm):
         widget=forms.RadioSelect(),
     )
 
+    users_per_page = forms.IntegerField(
+        label=_("Number of users displayed on a single page"), min_value=4
+    )
+    users_per_page_orphans = forms.IntegerField(
+        label=_("Maximum orphans"),
+        help_text=_(
+            "If number of users to be displayed on the last page is less or equal to "
+            "number specified in this setting, those users will instead be displayed "
+            "on previous page, reducing the total number of pages on the list."
+        ),
+        min_value=0,
+    )
+
+    top_posters_ranking_length = forms.IntegerField(
+        label=_("Maximum age in days of posts that should count to the ranking"),
+        min_value=1,
+    )
+    top_posters_ranking_size = forms.IntegerField(
+        label=_("Maximum number of ranked users"), min_value=2
+    )
+
+    allow_data_downloads = YesNoSwitch(label=_("Allow users to download their data"))
+    data_downloads_expiration = forms.IntegerField(
+        label=_("Maximum age in hours of data downloads before they expire"),
+        help_text=_(
+            "Data downloads older than specified will have their files deleted and "
+            "will be marked as expired."
+        ),
+        min_value=1,
+    )
+
+    allow_delete_own_account = YesNoSwitch(
+        label=_("Allow users to delete their own accounts")
+    )
+
+    ip_storage_time = forms.IntegerField(
+        label=_("IP storage time"),
+        help_text=_(
+            "Number of days for which users IP addresses are stored in forum database. "
+            "Enter zero to store registered IP addresses forever. Deleting user "
+            "account always deletes the IP addresses associated with it."
+        ),
+        min_value=0,
+    )
+
+    anonymous_username = forms.CharField(
+        label=_("Anonymous username"),
+        help_text=_(
+            "This username is displayed instead of delete user's actual name "
+            "next to their content."
+        ),
+        min_length=1,
+        max_length=15,
+        validators=[validate_username_content],
+    )
+
     def clean_blank_avatar(self):
         upload = self.cleaned_data.get("blank_avatar")
         if not upload or upload == self.initial.get("blank_avatar"):
@@ -129,3 +205,14 @@ class ChangeUsersSettingsForm(ChangeSettingsForm):
             )
 
         return upload
+
+    def clean(self):
+        cleaned_data = super().clean()
+        if cleaned_data.get("users_per_page_orphans") > cleaned_data.get(
+            "users_per_page"
+        ):
+            self.add_error(
+                "users_per_page_orphans",
+                _("This value must be lower than number of users per page."),
+            )
+        return cleaned_data

+ 98 - 0
misago/conf/admin/tests/test_blank_avatar_validation.py

@@ -0,0 +1,98 @@
+from io import BytesIO
+
+import pytest
+from PIL import Image
+
+from django.core.files.uploadedfile import SimpleUploadedFile
+from django.urls import reverse
+
+from ... import settings
+from ...models import Setting
+
+admin_link = reverse("misago:admin:settings:users:index")
+
+
+def create_image(width, height):
+    image = Image.new("RGBA", (width, height))
+    stream = BytesIO()
+    image.save(stream, "PNG")
+    stream.seek(0)
+    return SimpleUploadedFile("image.png", stream.read(), "image/jpeg")
+
+
+def submit_image(admin_client, image=""):
+    data = {
+        "account_activation": "user",
+        "username_length_min": 10,
+        "username_length_max": 10,
+        "anonymous_username": "Deleted",
+        "avatar_upload_limit": 2000,
+        "default_avatar": "gravatar",
+        "default_gravatar_fallback": "dynamic",
+        "signature_length_max": 100,
+        "blank_avatar": image,
+        "subscribe_start": "no",
+        "subscribe_reply": "no",
+        "users_per_page": 12,
+        "users_per_page_orphans": 4,
+        "top_posters_ranking_length": 10,
+        "top_posters_ranking_size": 10,
+        "allow_data_downloads": "no",
+        "data_downloads_expiration": 48,
+        "allow_delete_own_account": "no",
+        "new_inactive_accounts_delete": 0,
+        "ip_storage_time": 0,
+    }
+
+    return admin_client.post(admin_link, data)
+
+
+@pytest.fixture
+def setting(db):
+    return Setting.objects.get(setting="blank_avatar")
+
+
+@pytest.fixture
+def setting_with_value(admin_client, setting):
+    min_size = max(settings.MISAGO_AVATARS_SIZES)
+    image_file = create_image(min_size, min_size)
+    submit_image(admin_client, image_file)
+
+    setting.refresh_from_db()
+    return setting
+
+
+def test_uploaded_image_is_rejected_if_its_not_square(admin_client, setting):
+    image_file = create_image(100, 200)
+    submit_image(admin_client, image_file)
+
+    setting.refresh_from_db()
+    assert not setting.value
+
+
+def test_uploaded_image_is_rejected_if_its_smaller_than_max_avatar_size(
+    admin_client, setting
+):
+    min_size = max(settings.MISAGO_AVATARS_SIZES)
+    image_file = create_image(min_size - 1, min_size - 1)
+    submit_image(admin_client, image_file)
+
+    setting.refresh_from_db()
+    assert not setting.value
+
+
+def test_valid_blank_avatar_can_be_uploaded(admin_client, setting):
+    min_size = max(settings.MISAGO_AVATARS_SIZES)
+    image_file = create_image(min_size, min_size)
+    submit_image(admin_client, image_file)
+
+    setting.refresh_from_db()
+    assert setting.value
+
+
+def test_submitting_form_without_new_image_doesnt_unset_existing_image(
+    admin_client, setting_with_value
+):
+    submit_image(admin_client)
+    setting_with_value.refresh_from_db()
+    assert setting_with_value.value

+ 2 - 2
misago/conf/context_processors.py

@@ -52,8 +52,8 @@ def preload_settings_json(request):
         {
             "BLANK_AVATAR_URL": request.settings.blank_avatar or BLANK_AVATAR_URL,
             "CSRF_COOKIE_NAME": settings.CSRF_COOKIE_NAME,
-            "ENABLE_DELETE_OWN_ACCOUNT": settings.MISAGO_ENABLE_DELETE_OWN_ACCOUNT,
-            "ENABLE_DOWNLOAD_OWN_DATA": settings.MISAGO_ENABLE_DOWNLOAD_OWN_DATA,
+            "ENABLE_DELETE_OWN_ACCOUNT": request.settings.allow_delete_own_account,
+            "ENABLE_DOWNLOAD_OWN_DATA": request.settings.allow_data_downloads,
             "MISAGO_PATH": reverse("misago:index"),
             "SETTINGS": preloaded_settings,
             "STATIC_URL": settings.STATIC_URL,

+ 2 - 119
misago/conf/defaults.py

@@ -25,44 +25,12 @@ MISAGO_ACL_EXTENSIONS = [
 ]
 
 
-# Anonymous name used to replace deleted user's name in places that are keeping it
-
-MISAGO_ANONYMOUS_USERNAME = "Ghost"
-
-
-# Allow users to download their personal data
-# Enables users to learn what data about them is being held by the site without having
-# to contact site's administrators.
-
-MISAGO_ENABLE_DOWNLOAD_OWN_DATA = True
-
-# Number of hours for which user data should be available for download.
-# When data download is marked as expired, data archive associated with it is deleted.
-
-MISAGO_USER_DATA_DOWNLOADS_EXPIRE_IN_HOURS = 48
-
 # Path to the directory that Misago should use to prepare user data downloads.
 # Should not be accessible from internet.
 
 MISAGO_USER_DATA_DOWNLOADS_WORKING_DIR = None
 
 
-# Automatically delete new user accounts that weren't activated in specified time
-# If you rely on admin review of new registrations, make this period long, disable
-# the "deleteinactiveusers" management command, or change this value to zero. Otherwise
-# keep it short to give users a chance to retry on their own after few days pass.s
-
-MISAGO_DELETE_NEW_INACTIVE_USERS_OLDER_THAN_DAYS = 0
-
-
-# Allow users to delete their accounts
-# Lets users delete their own account on the site without having to contact site administrators.
-# This mechanism doesn't delete user posts, polls or attachments, but attempts to anonymize any
-# data about user left behind after user is deleted.
-
-MISAGO_ENABLE_DELETE_OWN_ACCOUNT = False
-
-
 # Custom markup extensions
 
 MISAGO_MARKUP_EXTENSIONS = []
@@ -127,12 +95,6 @@ MISAGO_SEARCH_EXTENSIONS = [
 ]
 
 
-# Misago-admin specific date formats
-
-MISAGO_COMPACT_DATE_FORMAT_DAY_MONTH = "j M"
-MISAGO_COMPACT_DATE_FORMAT_DAY_MONTH_YEAR = "M 'y"
-
-
 # Additional registration validators
 # https://misago.readthedocs.io/en/latest/developers/validating_registrations.html
 
@@ -147,12 +109,6 @@ MISAGO_NEW_REGISTRATIONS_VALIDATORS = [
 MISAGO_PROFILE_FIELDS = []
 
 
-# Stop Forum Spam settings
-
-MISAGO_USE_STOP_FORUM_SPAM = True
-MISAGO_STOP_FORUM_SPAM_MIN_CONFIDENCE = 80
-
-
 # Social Auth Backends Names Overrides
 # This seeting may be used to customise auth backends names displayed in the UI
 
@@ -186,19 +142,6 @@ MISAGO_ADMIN_SESSION_EXPIRATION = 60
 MISAGO_THREADS_ON_INDEX = True
 
 
-# Max age of notifications in days
-# Notifications older than this are deleted. On very active forums its better to keep this smaller.
-
-MISAGO_NOTIFICATIONS_MAX_AGE = 40
-
-
-# Fail-safe limits in case forum is raided by spambot
-# No user may exceed those limits, however you may disable them by changing them to 0.
-
-MISAGO_DIALY_POST_LIMIT = 600
-MISAGO_HOURLY_POST_LIMIT = 100
-
-
 # Function used for generating individual avatar for user
 
 MISAGO_DYNAMIC_AVATAR_DRAWER = "misago.users.avatars.dynamic.draw_default"
@@ -222,28 +165,6 @@ MISAGO_AVATARS_SIZES = [400, 200, 150, 100, 64, 50, 30]
 MISAGO_BLANK_AVATAR = "misago/img/blank-avatar.png"
 
 
-# Threads lists pagination settings
-
-MISAGO_THREADS_PER_PAGE = 25
-
-
-# Posts lists pagination settings
-
-MISAGO_POSTS_PER_PAGE = 18
-MISAGO_POSTS_TAIL = 6
-
-
-# Number of events displayed on single thread page
-# If there's more events than specified, oldest events will be trimmed
-
-MISAGO_EVENTS_PER_PAGE = 20
-
-
-# Number of attachments possible to assign to single post
-
-MISAGO_POST_ATTACHMENTS_LIMIT = 16
-
-
 # Max allowed size of image before Misago will generate thumbnail for it
 
 MISAGO_ATTACHMENT_IMAGE_SIZE_LIMIT = (500, 500)
@@ -253,49 +174,11 @@ MISAGO_ATTACHMENT_IMAGE_SIZE_LIMIT = (500, 500)
 
 MISAGO_ATTACHMENT_SECRET_LENGTH = 64
 
-# How old (in minutes) should attachments unassociated with any be before they'll
-# automatically deleted by "clearattachments" task
-
-MISAGO_ATTACHMENT_ORPHANED_EXPIRE = 24 * 60
-
 
 # Names of files served when user requests file that doesn't exist or is unavailable
-# Those files will be sought within STATIC_ROOT directory
-
-MISAGO_404_IMAGE = "misago/img/error-404.png"
-MISAGO_403_IMAGE = "misago/img/error-403.png"
-
-
-# Controls max age in days of items that Misago has to process to make rankings
-# Used for active posters and most liked users lists
-# If your forum runs out of memory when trying to generate users rankings list
-# or you want those to be more dynamic, give this setting lower value
-# You don't have to be overzelous with this as user rankings are cached for 24h
-
-MISAGO_RANKING_LENGTH = 30
-
-# Controls max number of items displayed on ranked lists
-
-MISAGO_RANKING_SIZE = 50
-
-
-# Specifies the number of days that IP addresses are stored in the database before removing.
-# Change this setting to None to never remove old IP addresses.
-
-MISAGO_IP_STORE_TIME = None
-
-
-# Controls number of users displayed on single page
-
-MISAGO_USERS_PER_PAGE = 12
-
-
-# Controls amount of data used by readtracking system
-# Items older than number of days specified below are considered read
-# Depending on amount of new content being posted on your forum you may want
-# To decrease or increase this number to fine-tune readtracker performance
 
-MISAGO_READTRACKER_CUTOFF = 40
+MISAGO_ATTACHMENT_403_IMAGE = "misago/img/attachment-403.png"
+MISAGO_ATTACHMENT_404_IMAGE = "misago/img/attachment-404.png"
 
 
 # Available Moment.js locales

+ 23 - 0
misago/conf/migrations/0004_create_settings.py

@@ -14,10 +14,13 @@ default_settings = [
         "dry_value": 1536,
         "is_public": True,
     },
+    {"setting": "attachment_403_image", "python_type": "image"},
+    {"setting": "attachment_404_image", "python_type": "image"},
     {"setting": "blank_avatar", "python_type": "image"},
     {"setting": "captcha_type", "dry_value": "no", "is_public": True},
     {"setting": "default_avatar", "dry_value": "gravatar"},
     {"setting": "default_gravatar_fallback", "dry_value": "dynamic"},
+    {"setting": "unused_attachments_lifetime", "python_type": "int", "dry_value": 24},
     {"setting": "email_footer"},
     {
         "setting": "forum_address",
@@ -34,6 +37,9 @@ default_settings = [
     {"setting": "logo", "python_type": "image", "is_public": True},
     {"setting": "logo_small", "python_type": "image", "is_public": True},
     {"setting": "logo_text", "dry_value": "Misago", "is_public": True},
+    {"setting": "daily_post_limit", "python_type": "int", "dry_value": 600},
+    {"setting": "hourly_post_limit", "python_type": "int", "dry_value": 100},
+    {"setting": "post_attachments_limit", "python_type": "int", "dry_value": 16},
     {
         "setting": "post_length_max",
         "python_type": "int",
@@ -46,6 +52,11 @@ default_settings = [
         "dry_value": 5,
         "is_public": True,
     },
+    {"setting": "readtracker_cutoff", "python_type": "int", "dry_value": 40},
+    {"setting": "threads_per_page", "python_type": "int", "dry_value": 26},
+    {"setting": "posts_per_page", "python_type": "int", "dry_value": 18},
+    {"setting": "posts_per_page_orphans", "python_type": "int", "dry_value": 6},
+    {"setting": "events_per_page", "python_type": "int", "dry_value": 20},
     {"setting": "og_image", "python_type": "image"},
     {
         "setting": "og_image_avatar_on_profile",
@@ -80,6 +91,18 @@ default_settings = [
     },
     {"setting": "username_length_min", "python_type": "int", "dry_value": 3},
     {"setting": "username_length_max", "python_type": "int", "dry_value": 14},
+    {"setting": "anonymous_username", "dry_value": "Deleted"},
+    {"setting": "enable_stop_forum_spam", "python_type": "bool", "dry_value": False},
+    {"setting": "stop_forum_spam_confidence", "python_type": "int", "dry_value": 80},
+    {"setting": "users_per_page", "python_type": "int", "dry_value": 12},
+    {"setting": "users_per_page_orphans", "python_type": "int", "dry_value": 4},
+    {"setting": "allow_data_downloads", "python_type": "bool", "dry_value": True},
+    {"setting": "data_downloads_expiration", "python_type": "int", "dry_value": 48},
+    {"setting": "allow_delete_own_account", "python_type": "bool", "dry_value": False},
+    {"setting": "top_posters_ranking_length", "python_type": "int", "dry_value": 30},
+    {"setting": "top_posters_ranking_size", "python_type": "int", "dry_value": 50},
+    {"setting": "new_inactive_accounts_delete", "python_type": "int", "dry_value": 0},
+    {"setting": "ip_storage_time", "python_type": "int", "dry_value": 90},
 ]
 
 removed_settings = ["forum_branding_display", "forum_branding_text"]

+ 7 - 0
misago/conf/shortcuts.py

@@ -0,0 +1,7 @@
+from ..cache.versions import get_cache_versions
+from .dynamicsettings import DynamicSettings
+
+
+def get_dynamic_settings():
+    cache_versions = get_cache_versions()
+    return DynamicSettings(cache_versions)

+ 64 - 0
misago/conftest.py

@@ -7,6 +7,7 @@ from .conf import SETTINGS_CACHE
 from .conf.dynamicsettings import DynamicSettings
 from .conf.staticsettings import StaticSettings
 from .themes import THEME_CACHE
+from .threads.test import post_thread
 from .users import BANS_CACHE
 from .users.models import AnonymousUser
 from .users.test import create_test_superuser, create_test_user
@@ -47,6 +48,11 @@ def anonymous_user():
 
 
 @pytest.fixture
+def anonymous_user_acl(anonymous_user, cache_versions):
+    return useracl.get_user_acl(anonymous_user, cache_versions)
+
+
+@pytest.fixture
 def user(db, user_password):
     return create_test_user("User", "user@example.com", user_password)
 
@@ -108,6 +114,14 @@ def other_superuser(db, user_password):
 
 
 @pytest.fixture
+def user_client(mocker, client, user):
+    client.force_login(user)
+    session = client.session
+    session.save()
+    return client
+
+
+@pytest.fixture
 def admin_client(mocker, client, superuser):
     client.force_login(superuser)
     session = client.session
@@ -133,3 +147,53 @@ def root_category(db):
 @pytest.fixture
 def default_category(db):
     return Category.objects.get(slug="first-category")
+
+
+@pytest.fixture
+def thread(default_category):
+    return post_thread(default_category)
+
+
+@pytest.fixture
+def hidden_thread(default_category):
+    return post_thread(default_category, is_hidden=True)
+
+
+@pytest.fixture
+def unapproved_thread(default_category):
+    return post_thread(default_category, is_unapproved=True)
+
+
+@pytest.fixture
+def post(thread):
+    return thread.first_post
+
+
+@pytest.fixture
+def user_thread(default_category, user):
+    return post_thread(default_category, poster=user)
+
+
+@pytest.fixture
+def user_hidden_thread(default_category, user):
+    return post_thread(default_category, poster=user, is_hidden=True)
+
+
+@pytest.fixture
+def user_unapproved_thread(default_category, user):
+    return post_thread(default_category, poster=user, is_unapproved=True)
+
+
+@pytest.fixture
+def other_user_thread(default_category, other_user):
+    return post_thread(default_category, poster=other_user)
+
+
+@pytest.fixture
+def other_user_hidden_thread(default_category, other_user):
+    return post_thread(default_category, poster=other_user, is_hidden=True)
+
+
+@pytest.fixture
+def other_user_unapproved_thread(default_category, other_user):
+    return post_thread(default_category, poster=other_user, is_unapproved=True)

+ 1 - 1
misago/core/exceptionhandler.py

@@ -64,7 +64,7 @@ def handle_permission_denied_exception(request, exception):
 
 
 def handle_social_auth_exception(request, exception):
-    social_logger.error(exception)
+    social_logger.error(exception, exc_info=exception)
     return errorpages.social_auth_failed(request, exception)
 
 

+ 7 - 7
misago/core/tests/test_mail.py

@@ -22,6 +22,7 @@ class MailTests(TestCase):
                 user, "Misago Test Mail", "misago/emails/base", context={"settings": {}}
             )
 
+    @override_dynamic_settings(forum_address="http://test.com/")
     def test_mail_user(self):
         """mail_user sets message in backend"""
         user = create_test_user("User", "user@example.com")
@@ -29,13 +30,12 @@ class MailTests(TestCase):
         cache_versions = get_cache_versions()
         settings = DynamicSettings(cache_versions)
 
-        with override_dynamic_settings(forum_address="http://test.com"):
-            mail_user(
-                user,
-                "Misago Test Mail",
-                "misago/emails/base",
-                context={"settings": settings},
-            )
+        mail_user(
+            user,
+            "Misago Test Mail",
+            "misago/emails/base",
+            context={"settings": settings},
+        )
 
         self.assertEqual(mail.outbox[0].subject, "Misago Test Mail")
 

+ 4 - 1
misago/legal/admin/__init__.py

@@ -32,5 +32,8 @@ class MisagoAdminExtension:
 
     def register_navigation_nodes(self, site):
         site.add_node(
-            name=_("Legal agreements"), parent="settings", namespace="agreements"
+            name=_("Legal agreements"),
+            description=_("Set terms of service and privacy policy contents."),
+            parent="settings",
+            namespace="agreements",
         )

+ 7 - 7
misago/readtracker/categoriestracker.py

@@ -1,9 +1,9 @@
 from ..threads.models import Post, Thread
 from ..threads.permissions import exclude_invisible_posts, exclude_invisible_threads
-from .dates import get_cutoff_date
+from .cutoffdate import get_cutoff_date
 
 
-def make_read_aware(user, user_acl, categories):
+def make_read_aware(request, categories):
     if not categories:
         return
 
@@ -12,24 +12,24 @@ def make_read_aware(user, user_acl, categories):
 
     make_read(categories)
 
-    if user.is_anonymous:
+    if request.user.is_anonymous:
         return
 
     threads = Thread.objects.filter(category__in=categories)
-    threads = exclude_invisible_threads(user_acl, categories, threads)
+    threads = exclude_invisible_threads(request.user_acl, categories, threads)
 
     queryset = (
         Post.objects.filter(
             category__in=categories,
             thread__in=threads,
-            posted_on__gt=get_cutoff_date(user),
+            posted_on__gt=get_cutoff_date(request.settings, request.user),
         )
         .values_list("category", flat=True)
         .distinct()
     )
 
-    queryset = queryset.exclude(id__in=user.postread_set.values("post"))
-    queryset = exclude_invisible_posts(user_acl, categories, queryset)
+    queryset = queryset.exclude(id__in=request.user.postread_set.values("post"))
+    queryset = exclude_invisible_posts(request.user_acl, categories, queryset)
 
     unread_categories = list(queryset)
 

+ 13 - 0
misago/readtracker/cutoffdate.py

@@ -0,0 +1,13 @@
+from datetime import timedelta
+
+from django.utils import timezone
+
+from ..conf import settings
+
+
+def get_cutoff_date(settings, user=None):
+    cutoff_date = timezone.now() - timedelta(days=settings.readtracker_cutoff)
+
+    if user and user.is_authenticated and user.joined_on > cutoff_date:
+        return user.joined_on
+    return cutoff_date

+ 0 - 23
misago/readtracker/dates.py

@@ -1,23 +0,0 @@
-from datetime import timedelta
-
-from django.utils import timezone
-
-from ..conf import settings
-
-
-def get_cutoff_date(user=None):
-    cutoff_date = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF)
-
-    if user and user.is_authenticated and user.joined_on > cutoff_date:
-        return user.joined_on
-    return cutoff_date
-
-
-def is_date_tracked(date, user):
-    if not date:
-        return False
-
-    cutoff_date = get_cutoff_date()
-    if cutoff_date < user.joined_on:
-        cutoff_date = user.joined_on
-    return date > cutoff_date

+ 4 - 3
misago/readtracker/management/commands/clearreadtracker.py

@@ -1,6 +1,7 @@
 from django.core.management.base import BaseCommand
 
-from ...dates import get_cutoff_date
+from ....conf.shortcuts import get_dynamic_settings
+from ...cutoffdate import get_cutoff_date
 from ...models import PostRead
 
 
@@ -8,12 +9,12 @@ class Command(BaseCommand):
     help = "Deletes expired records from readtracker"
 
     def handle(self, *args, **options):
-        queryset = PostRead.objects.filter(last_read_on__lt=get_cutoff_date())
+        settings = get_dynamic_settings()
+        queryset = PostRead.objects.filter(last_read_on__lt=get_cutoff_date(settings))
         deleted_count = queryset.count()
 
         if deleted_count:
             queryset.delete()
-
             message = "\n\nDeleted %s expired entries" % deleted_count
         else:
             message = "\n\nNo expired entries were found"

+ 1 - 69
misago/readtracker/migrations/0003_migrate_reads_to_posts.py

@@ -1,77 +1,9 @@
 # Generated by Django 1.11.5 on 2017-10-07 14:49
-from datetime import timedelta
-
-from django.conf import settings
 from django.db import migrations
-from django.utils import timezone
-
-from ...conf import defaults
-
-try:
-    READS_CUTOFF = settings.MISAGO_READTRACKER_CUTOFF
-except AttributeError:
-    READS_CUTOFF = defaults.MISAGO_READTRACKER_CUTOFF
-
-
-def populate_posts_reads(apps, schema_editor):
-    reads_cutoff = timezone.now() - timedelta(days=READS_CUTOFF)
-
-    Post = apps.get_model("misago_threads", "Post")
-
-    CategoryRead = apps.get_model("misago_readtracker", "CategoryRead")
-    ThreadRead = apps.get_model("misago_readtracker", "ThreadRead")
-    PostRead = apps.get_model("misago_readtracker", "PostRead")
-
-    migrated_reads = {}
-
-    # read posts by category reads
-    queryset = CategoryRead.objects.select_related().iterator()
-    for category_read in queryset:
-        posts_queryset = Post.objects.filter(
-            category=category_read.category, posted_on__gte=reads_cutoff
-        )
-
-        for post in posts_queryset.iterator():
-            PostRead.objects.create(
-                user=category_read.user,
-                category=post.category,
-                thread=post.thread,
-                post=post,
-                last_read_on=post.posted_on,
-            )
-
-            if category_read.user.pk in migrated_reads:
-                migrated_reads[category_read.user.pk].append(post.pk)
-            else:
-                migrated_reads[category_read.user.pk] = [post.pk]
-
-    # read posts by thread reads
-    queryset = ThreadRead.objects.select_related().iterator()
-    for thread_read in queryset:
-        posts_queryset = Post.objects.filter(
-            thread=thread_read.thread, posted_on__gte=reads_cutoff
-        )
-
-        for post in posts_queryset.iterator():
-            if post.pk in migrated_reads.get(thread_read.user.pk, []):
-                continue
-
-            PostRead.objects.create(
-                user=thread_read.user,
-                category=post.category,
-                thread=post.thread,
-                post=post,
-                last_read_on=post.posted_on,
-            )
-
-
-def noop_reverse(apps, schema_editor):
-    PostRead = apps.get_model("misago_readtracker", "PostRead")
-    PostRead.objects.all().delete()
 
 
 class Migration(migrations.Migration):
 
     dependencies = [("misago_readtracker", "0002_postread")]
 
-    operations = [migrations.RunPython(populate_posts_reads, noop_reverse)]
+    operations = []

+ 5 - 5
misago/readtracker/poststracker.py

@@ -1,7 +1,7 @@
-from .dates import get_cutoff_date
+from .cutoffdate import get_cutoff_date
 
 
-def make_read_aware(user, posts):
+def make_read_aware(request, posts):
     if not posts:
         return
 
@@ -10,10 +10,10 @@ def make_read_aware(user, posts):
 
     make_read(posts)
 
-    if user.is_anonymous:
+    if request.user.is_anonymous:
         return
 
-    cutoff_date = get_cutoff_date(user)
+    cutoff_date = get_cutoff_date(request.settings, request.user)
     unresolved_posts = {}
 
     for post in posts:
@@ -23,7 +23,7 @@ def make_read_aware(user, posts):
             unresolved_posts[post.pk] = post
 
     if unresolved_posts:
-        queryset = user.postread_set.filter(post__in=unresolved_posts)
+        queryset = request.user.postread_set.filter(post__in=unresolved_posts)
         for post_id in queryset.values_list("post_id", flat=True):
             unresolved_posts[post_id].is_read = True
             unresolved_posts[post_id].is_new = False

+ 23 - 0
misago/readtracker/tests/conftest.py

@@ -0,0 +1,23 @@
+from unittest.mock import Mock
+
+import pytest
+
+from ..poststracker import save_read
+
+
+@pytest.fixture
+def read_thread(user, thread):
+    save_read(user, thread.first_post)
+    return thread
+
+
+@pytest.fixture
+def anonymous_request_mock(dynamic_settings, anonymous_user, anonymous_user_acl):
+    return Mock(
+        settings=dynamic_settings, user=anonymous_user, user_acl=anonymous_user_acl
+    )
+
+
+@pytest.fixture
+def request_mock(dynamic_settings, user, user_acl):
+    return Mock(settings=dynamic_settings, user=user, user_acl=user_acl)

+ 161 - 172
misago/readtracker/tests/test_categoriestracker.py

@@ -1,194 +1,183 @@
 from datetime import timedelta
 
-from django.test import TestCase
+import pytest
 from django.utils import timezone
 
-from .. import categoriestracker, poststracker
-from ...acl.useracl import get_user_acl
-from ...categories.models import Category
-from ...conf import settings
-from ...conftest import get_cache_versions
-from ...threads import test
-from ...users.test import create_test_user
+from ...conf.test import override_dynamic_settings
+from ...threads.test import reply_thread
+from ..categoriestracker import make_read_aware
+from ..poststracker import save_read
 
-cache_versions = get_cache_versions()
 
+def remove_tracking(thread):
+    thread.started_on = timezone.now() - timedelta(days=4)
+    thread.save()
+    thread.first_post.posted_on = thread.started_on
+    thread.first_post.save()
 
-class AnonymousUser:
-    is_authenticated = False
-    is_anonymous = True
 
+@pytest.fixture
+def read_thread(user, thread):
+    save_read(user, thread.first_post)
+    return thread
 
-class CategoriesTrackerTests(TestCase):
-    def setUp(self):
-        self.user = create_test_user("User", "user@example.com")
-        self.user_acl = get_user_acl(self.user, cache_versions)
-        self.category = Category.objects.get(slug="first-category")
 
-    def test_falsy_value(self):
-        """passing falsy value to readtracker causes no errors"""
-        categoriestracker.make_read_aware(self.user, self.user_acl, None)
-        categoriestracker.make_read_aware(self.user, self.user_acl, False)
-        categoriestracker.make_read_aware(self.user, self.user_acl, [])
+def test_falsy_value_can_be_made_read_aware(request_mock):
+    make_read_aware(request_mock, None)
+    make_read_aware(request_mock, False)
 
-    def test_anon_thread_before_cutoff(self):
-        """non-tracked thread is marked as read for anonymous users"""
-        started_on = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF)
-        test.post_thread(self.category, started_on=started_on)
 
-        categoriestracker.make_read_aware(AnonymousUser(), None, self.category)
-        self.assertTrue(self.category.is_read)
-        self.assertFalse(self.category.is_new)
+def test_empty_list_can_be_made_read_aware(request_mock):
+    make_read_aware(request_mock, [])
 
-    def test_anon_thread_after_cutoff(self):
-        """tracked thread is marked as read for anonymous users"""
-        test.post_thread(self.category, started_on=timezone.now())
 
-        categoriestracker.make_read_aware(AnonymousUser(), None, self.category)
-        self.assertTrue(self.category.is_read)
-        self.assertFalse(self.category.is_new)
+def test_empty_category_is_marked_as_read(request_mock, default_category):
+    make_read_aware(request_mock, default_category)
+    assert default_category.is_read
+    assert not default_category.is_new
 
-    def test_user_thread_before_cutoff(self):
-        """non-tracked thread is marked as read for authenticated users"""
-        started_on = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF)
-        test.post_thread(self.category, started_on=started_on)
 
-        categoriestracker.make_read_aware(self.user, self.user_acl, self.category)
-        self.assertTrue(self.category.is_read)
-        self.assertFalse(self.category.is_new)
+def test_category_with_tracked_post_is_marked_as_read(
+    request_mock, post, default_category
+):
+    make_read_aware(request_mock, default_category)
+    assert not default_category.is_read
+    assert default_category.is_new
 
-    def test_user_unread_thread(self):
-        """tracked thread is marked as unread for authenticated users"""
-        test.post_thread(self.category, started_on=timezone.now())
 
-        categoriestracker.make_read_aware(self.user, self.user_acl, self.category)
-        self.assertFalse(self.category.is_read)
-        self.assertTrue(self.category.is_new)
+def test_category_with_post_older_than_user_is_marked_as_read(
+    request_mock, post, default_category
+):
+    post.posted_on = timezone.now() - timedelta(days=1)
+    post.save()
 
-    def test_user_created_after_thread(self):
-        """tracked thread older than user is marked as read"""
-        started_on = timezone.now() - timedelta(days=1)
-        test.post_thread(self.category, started_on=started_on)
+    make_read_aware(request_mock, default_category)
+    assert default_category.is_read
+    assert not default_category.is_new
 
-        categoriestracker.make_read_aware(self.user, self.user_acl, self.category)
-        self.assertTrue(self.category.is_read)
-        self.assertFalse(self.category.is_new)
 
-    def test_user_read_post(self):
-        """tracked thread with read post marked as read"""
-        thread = test.post_thread(self.category, started_on=timezone.now())
-
-        poststracker.save_read(self.user, thread.first_post)
-
-        categoriestracker.make_read_aware(self.user, self.user_acl, self.category)
-        self.assertTrue(self.category.is_read)
-        self.assertFalse(self.category.is_new)
-
-    def test_user_first_unread_last_read_post(self):
-        """tracked thread with unread first and last read post marked as unread"""
-        thread = test.post_thread(self.category, started_on=timezone.now())
-
-        post = test.reply_thread(thread, posted_on=timezone.now())
-        poststracker.save_read(self.user, post)
-
-        categoriestracker.make_read_aware(self.user, self.user_acl, self.category)
-        self.assertFalse(self.category.is_read)
-        self.assertTrue(self.category.is_new)
-
-    def test_user_first_read_post_unread_event(self):
-        """tracked thread with read first post and unread event"""
-        thread = test.post_thread(self.category, started_on=timezone.now())
-        poststracker.save_read(self.user, thread.first_post)
-
-        test.reply_thread(thread, posted_on=timezone.now(), is_event=True)
-
-        categoriestracker.make_read_aware(self.user, self.user_acl, self.category)
-        self.assertFalse(self.category.is_read)
-        self.assertTrue(self.category.is_new)
-
-    def test_user_hidden_event(self):
-        """tracked thread with unread first post and hidden event"""
-        thread = test.post_thread(self.category, started_on=timezone.now())
+@override_dynamic_settings(readtracker_cutoff=3)
+def test_category_with_post_older_than_cutoff_is_marked_as_read(
+    request_mock, user, post, default_category
+):
+    user.joined_on = timezone.now() - timedelta(days=5)
+    user.save()
 
-        test.reply_thread(
-            thread, posted_on=timezone.now(), is_event=True, is_hidden=True
-        )
-
-        categoriestracker.make_read_aware(self.user, self.user_acl, self.category)
-        self.assertFalse(self.category.is_read)
-        self.assertTrue(self.category.is_new)
-
-    def test_user_first_read_post_hidden_event(self):
-        """tracked thread with read first post and hidden event"""
-        thread = test.post_thread(self.category, started_on=timezone.now())
-        poststracker.save_read(self.user, thread.first_post)
-
-        test.reply_thread(
-            thread, posted_on=timezone.now(), is_event=True, is_hidden=True
-        )
-
-        categoriestracker.make_read_aware(self.user, self.user_acl, self.category)
-        self.assertTrue(self.category.is_read)
-        self.assertFalse(self.category.is_new)
-
-    def test_user_thread_before_cutoff_unread_post(self):
-        """non-tracked thread is marked as unread for anonymous users"""
-        started_on = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF)
-        test.post_thread(self.category, started_on=started_on)
-
-        categoriestracker.make_read_aware(self.user, self.user_acl, self.category)
-        self.assertTrue(self.category.is_read)
-        self.assertFalse(self.category.is_new)
-
-    def test_user_first_read_post_unapproved_post(self):
-        """tracked thread with read first post and unapproved post"""
-        thread = test.post_thread(self.category, started_on=timezone.now())
-        poststracker.save_read(self.user, thread.first_post)
-
-        test.reply_thread(thread, posted_on=timezone.now(), is_unapproved=True)
-
-        categoriestracker.make_read_aware(self.user, self.user_acl, self.category)
-        self.assertTrue(self.category.is_read)
-        self.assertFalse(self.category.is_new)
-
-    def test_user_first_read_post_unapproved_own_post(self):
-        """tracked thread with read first post and unapproved own post"""
-        thread = test.post_thread(self.category, started_on=timezone.now())
-        poststracker.save_read(self.user, thread.first_post)
-
-        test.reply_thread(
-            thread, posted_on=timezone.now(), poster=self.user, is_unapproved=True
-        )
-
-        categoriestracker.make_read_aware(self.user, self.user_acl, self.category)
-        self.assertFalse(self.category.is_read)
-        self.assertTrue(self.category.is_new)
-
-    def test_user_unapproved_thread_unread_post(self):
-        """tracked unapproved thread"""
-        test.post_thread(self.category, started_on=timezone.now(), is_unapproved=True)
-
-        categoriestracker.make_read_aware(self.user, self.user_acl, self.category)
-        self.assertTrue(self.category.is_read)
-        self.assertFalse(self.category.is_new)
-
-    def test_user_unapproved_own_thread_unread_post(self):
-        """tracked unapproved but visible thread"""
-        test.post_thread(
-            self.category,
-            poster=self.user,
-            started_on=timezone.now(),
-            is_unapproved=True,
-        )
-
-        categoriestracker.make_read_aware(self.user, self.user_acl, self.category)
-        self.assertFalse(self.category.is_read)
-        self.assertTrue(self.category.is_new)
-
-    def test_user_hidden_thread_unread_post(self):
-        """tracked hidden thread"""
-        test.post_thread(self.category, started_on=timezone.now(), is_hidden=True)
-
-        categoriestracker.make_read_aware(self.user, self.user_acl, self.category)
-        self.assertTrue(self.category.is_read)
-        self.assertFalse(self.category.is_new)
+    post.posted_on = timezone.now() - timedelta(days=4)
+    post.save()
+
+    make_read_aware(request_mock, default_category)
+    assert default_category.is_read
+    assert not default_category.is_new
+
+
+def test_category_with_read_post_is_marked_as_read(
+    request_mock, user, post, default_category
+):
+    save_read(user, post)
+    make_read_aware(request_mock, default_category)
+    assert default_category.is_read
+    assert not default_category.is_new
+
+
+def test_category_with_post_in_thread_older_than_user_is_marked_as_unread(
+    request_mock, thread, default_category
+):
+    remove_tracking(thread)
+    reply_thread(thread)
+    make_read_aware(request_mock, default_category)
+    assert not default_category.is_read
+    assert default_category.is_new
+
+
+def test_category_with_post_in_invisible_thread_is_marked_as_read(
+    request_mock, hidden_thread, default_category
+):
+    reply_thread(hidden_thread)
+    make_read_aware(request_mock, default_category)
+    assert default_category.is_read
+    assert not default_category.is_new
+
+
+def test_category_with_read_post_in_thread_older_than_user_is_marked_as_read(
+    request_mock, user, thread, default_category
+):
+    remove_tracking(thread)
+    post = reply_thread(thread)
+    save_read(user, post)
+    make_read_aware(request_mock, default_category)
+    assert default_category.is_read
+    assert not default_category.is_new
+
+
+def test_category_with_read_post_in_read_thread_is_marked_as_read(
+    request_mock, user, read_thread, default_category
+):
+    post = reply_thread(read_thread)
+    save_read(user, post)
+    make_read_aware(request_mock, default_category)
+    assert default_category.is_read
+    assert not default_category.is_new
+
+
+def test_category_with_invisible_post_in_read_thread_is_marked_as_read(
+    request_mock, user, read_thread, default_category
+):
+    reply_thread(read_thread, is_unapproved=True)
+    make_read_aware(request_mock, default_category)
+    assert default_category.is_read
+    assert not default_category.is_new
+
+
+def test_category_with_new_event_in_read_thread_is_marked_as_unread(
+    request_mock, read_thread, default_category
+):
+    reply_thread(read_thread, is_event=True)
+    make_read_aware(request_mock, default_category)
+    assert not default_category.is_read
+    assert default_category.is_new
+
+
+def test_category_with_hidden_event_in_read_thread_is_marked_as_read(
+    request_mock, read_thread, default_category
+):
+    reply_thread(read_thread, is_hidden=True, is_event=True)
+    make_read_aware(request_mock, default_category)
+    assert default_category.is_read
+    assert not default_category.is_new
+
+
+def test_category_with_hidden_event_visible_to_user_in_read_thread_is_marked_as_unread(
+    request_mock, read_thread, default_category
+):
+    request_mock.user_acl["categories"][default_category.id]["can_hide_events"] = 1
+    reply_thread(read_thread, is_hidden=True, is_event=True)
+    make_read_aware(request_mock, default_category)
+    assert not default_category.is_read
+    assert default_category.is_new
+
+
+def test_empty_category_is_marked_as_read_for_anonymous_user(
+    anonymous_request_mock, default_category
+):
+    make_read_aware(anonymous_request_mock, default_category)
+    assert default_category.is_read
+    assert not default_category.is_new
+
+
+def test_category_with_tracked_thread_is_marked_as_read_for_anonymous_user(
+    anonymous_request_mock, thread, default_category
+):
+    make_read_aware(anonymous_request_mock, default_category)
+    assert default_category.is_read
+    assert not default_category.is_new
+
+
+@override_dynamic_settings(readtracker_cutoff=3)
+def test_category_with_non_tracked_thread_is_marked_as_read_for_anonymous_user(
+    anonymous_request_mock, thread, default_category
+):
+    remove_tracking(thread)
+    make_read_aware(anonymous_request_mock, default_category)
+    assert default_category.is_read
+    assert not default_category.is_new

+ 0 - 64
misago/readtracker/tests/test_clearreadtracker.py

@@ -1,64 +0,0 @@
-from datetime import timedelta
-from io import StringIO
-
-from django.core.management import call_command
-from django.test import TestCase
-from django.utils import timezone
-
-from ...categories.models import Category
-from ...conf import settings
-from ...threads import test
-from ...users.test import create_test_user
-from ..management.commands import clearreadtracker
-from ..models import PostRead
-
-
-class ClearReadTrackerTests(TestCase):
-    def setUp(self):
-        self.user = create_test_user("User", "user@example.com")
-        self.other_user = create_test_user("OtherUser", "otheruser@example.com")
-
-        self.category = Category.objects.get(slug="first-category")
-
-    def test_no_deleted(self):
-        """command works when there are no attachments"""
-        command = clearreadtracker.Command()
-
-        out = StringIO()
-        call_command(command, stdout=out)
-        command_output = out.getvalue().strip()
-
-        self.assertEqual(command_output, "No expired entries were found")
-
-    def test_delete_expired_entries(self):
-        """test deletes one expired tracker entry, but spares the other"""
-        thread = test.post_thread(self.category)
-
-        existing = PostRead.objects.create(
-            user=self.user,
-            category=self.category,
-            thread=thread,
-            post=thread.first_post,
-            last_read_on=timezone.now()
-            - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF / 4),
-        )
-        deleted = PostRead.objects.create(
-            user=self.other_user,
-            category=self.category,
-            thread=thread,
-            post=thread.first_post,
-            last_read_on=timezone.now()
-            - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF * 2),
-        )
-
-        command = clearreadtracker.Command()
-
-        out = StringIO()
-        call_command(command, stdout=out)
-        command_output = out.getvalue().strip()
-
-        self.assertEqual(command_output, "Deleted 1 expired entries")
-
-        PostRead.objects.get(pk=existing.pk)
-        with self.assertRaises(PostRead.DoesNotExist):
-            PostRead.objects.get(pk=deleted.pk)

+ 52 - 0
misago/readtracker/tests/test_clearreadtracker_command.py

@@ -0,0 +1,52 @@
+from datetime import timedelta
+from io import StringIO
+
+from django.core import management
+from django.utils import timezone
+
+from ...conf.test import override_dynamic_settings
+from ..management.commands import clearreadtracker
+from ..models import PostRead
+
+
+def call_command():
+    command = clearreadtracker.Command()
+
+    out = StringIO()
+    management.call_command(command, stdout=out)
+    return out.getvalue().strip().splitlines()[-1].strip()
+
+
+def test_command_works_if_there_are_no_read_tracker_entries(db):
+    command_output = call_command()
+    assert command_output == "No expired entries were found"
+
+
+@override_dynamic_settings(readtracker_cutoff=5)
+def test_recent_read_tracker_entry_is_not_cleared(user, post):
+    existing = PostRead.objects.create(
+        user=user,
+        category=post.category,
+        thread=post.thread,
+        post=post,
+        last_read_on=timezone.now(),
+    )
+
+    command_output = call_command()
+    assert command_output == "No expired entries were found"
+    assert PostRead.objects.exists()
+
+
+@override_dynamic_settings(readtracker_cutoff=5)
+def test_old_read_tracker_entry_is_cleared(user, post):
+    existing = PostRead.objects.create(
+        user=user,
+        category=post.category,
+        thread=post.thread,
+        post=post,
+        last_read_on=timezone.now() - timedelta(days=10),
+    )
+
+    command_output = call_command()
+    assert command_output == "Deleted 1 expired entries"
+    assert not PostRead.objects.exists()

+ 41 - 0
misago/readtracker/tests/test_cutoff_date.py

@@ -0,0 +1,41 @@
+from datetime import timedelta
+from unittest.mock import Mock
+
+from django.utils import timezone
+
+from ...conf.test import override_dynamic_settings
+from ..cutoffdate import get_cutoff_date
+
+
+def test_cutoff_date_for_no_user_is_calculated_from_setting(dynamic_settings):
+    cutoff_date = get_cutoff_date(dynamic_settings)
+    valid_cutoff_date = timezone.now() - timedelta(
+        days=dynamic_settings.readtracker_cutoff
+    )
+    assert cutoff_date < valid_cutoff_date
+
+
+def test_cutoff_date_for_recently_joined_user_is_their_join_date(dynamic_settings):
+    user = Mock(is_authenticated=True, joined_on=timezone.now())
+    cutoff_date = get_cutoff_date(dynamic_settings, user)
+    assert cutoff_date == user.joined_on
+
+
+@override_dynamic_settings(readtracker_cutoff=5)
+def test_cutoff_date_for_old_user_is_calculated_from_setting(dynamic_settings):
+    user = Mock(is_authenticated=True, joined_on=timezone.now() - timedelta(days=6))
+    cutoff_date = get_cutoff_date(dynamic_settings, user)
+    valid_cutoff_date = timezone.now() - timedelta(
+        days=dynamic_settings.readtracker_cutoff
+    )
+    assert cutoff_date < valid_cutoff_date
+    assert cutoff_date > user.joined_on
+
+
+def test_cutoff_date_for_anonymous_user_is_calculated_from_setting(dynamic_settings):
+    user = Mock(is_authenticated=False)
+    cutoff_date = get_cutoff_date(dynamic_settings, user)
+    valid_cutoff_date = timezone.now() - timedelta(
+        days=dynamic_settings.readtracker_cutoff
+    )
+    assert cutoff_date < valid_cutoff_date

+ 0 - 62
misago/readtracker/tests/test_dates.py

@@ -1,62 +0,0 @@
-from datetime import timedelta
-
-from django.test import TestCase
-from django.utils import timezone
-
-from ...conf import settings
-from ..dates import get_cutoff_date, is_date_tracked
-
-
-class MockUser:
-    is_authenticated = True
-
-    def __init__(self):
-        self.joined_on = timezone.now()
-
-
-class MockAnonymousUser:
-    is_authenticated = False
-
-
-class ReadTrackerDatesTests(TestCase):
-    def test_get_cutoff_date_no_user(self):
-        """get_cutoff_date utility works without user argument"""
-        valid_cutoff_date = timezone.now() - timedelta(
-            days=settings.MISAGO_READTRACKER_CUTOFF
-        )
-        returned_cutoff_date = get_cutoff_date()
-
-        self.assertTrue(returned_cutoff_date > valid_cutoff_date)
-
-    def test_get_cutoff_date_user(self):
-        """get_cutoff_date utility works with user argument"""
-        user = MockUser()
-
-        valid_cutoff_date = timezone.now() - timedelta(
-            days=settings.MISAGO_READTRACKER_CUTOFF
-        )
-        returned_cutoff_date = get_cutoff_date(user)
-
-        self.assertTrue(returned_cutoff_date > valid_cutoff_date)
-        self.assertEqual(returned_cutoff_date, user.joined_on)
-
-    def test_get_cutoff_date_anonymous_user(self):
-        """passing anonymous user to get_cutoff_date has no effect"""
-        user = MockAnonymousUser()
-
-        valid_cutoff_date = timezone.now() - timedelta(
-            days=settings.MISAGO_READTRACKER_CUTOFF
-        )
-        returned_cutoff_date = get_cutoff_date(user)
-
-        self.assertTrue(returned_cutoff_date > valid_cutoff_date)
-
-    def test_is_date_tracked(self):
-        """is_date_tracked validates dates"""
-        self.assertFalse(is_date_tracked(None, MockUser()))
-
-        past_date = timezone.now() - timedelta(minutes=10)
-        self.assertFalse(is_date_tracked(past_date, MockUser()))
-
-        future_date = timezone.now() + timedelta(minutes=10)
-        self.assertTrue(is_date_tracked(future_date, MockUser()))

+ 66 - 58
misago/readtracker/tests/test_poststracker.py

@@ -1,81 +1,89 @@
 from datetime import timedelta
 
-from django.test import TestCase
+import pytest
 from django.utils import timezone
 
-from ...categories.models import Category
-from ...conf import settings
-from ...threads import test
-from ...users.test import create_test_user
+from ...conf.test import override_dynamic_settings
 from ..poststracker import make_read_aware, save_read
 
 
-class AnonymousUser:
-    is_authenticated = False
-    is_anonymous = True
+def test_falsy_value_can_be_made_read_aware(request_mock):
+    make_read_aware(request_mock, None)
+    make_read_aware(request_mock, False)
 
 
-class PostsTrackerTests(TestCase):
-    def setUp(self):
-        self.user = create_test_user("User", "user@example.com")
-        self.category = Category.objects.get(slug="first-category")
-        self.thread = test.post_thread(self.category)
+def test_empty_list_can_be_made_read_aware(request_mock):
+    make_read_aware(request_mock, [])
 
-    def test_falsy_value(self):
-        """passing falsy value to readtracker causes no errors"""
-        make_read_aware(self.user, None)
-        make_read_aware(self.user, False)
-        make_read_aware(self.user, [])
 
-    def test_anon_post_before_cutoff(self):
-        """non-tracked post is marked as read for anonymous users"""
-        posted_on = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF)
-        post = test.reply_thread(self.thread, posted_on=posted_on)
+@pytest.fixture
+def read_post(user, post):
+    save_read(user, post)
+    return post
 
-        make_read_aware(AnonymousUser(), post)
-        self.assertTrue(post.is_read)
-        self.assertFalse(post.is_new)
 
-    def test_anon_post_after_cutoff(self):
-        """tracked post is marked as read for anonymous users"""
-        post = test.reply_thread(self.thread, posted_on=timezone.now())
+def test_tracked_post_is_marked_as_not_read_and_new(request_mock, post):
+    make_read_aware(request_mock, post)
+    assert not post.is_read
+    assert post.is_new
 
-        make_read_aware(AnonymousUser(), post)
-        self.assertTrue(post.is_read)
-        self.assertFalse(post.is_new)
 
-    def test_user_post_before_cutoff(self):
-        """untracked post is marked as read for authenticated users"""
-        posted_on = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF)
-        post = test.reply_thread(self.thread, posted_on=posted_on)
+@override_dynamic_settings(readtracker_cutoff=3)
+def test_not_tracked_post_is_marked_as_read_and_not_new(request_mock, post):
+    post.posted_on = timezone.now() - timedelta(days=4)
+    post.save()
 
-        make_read_aware(self.user, post)
-        self.assertTrue(post.is_read)
-        self.assertFalse(post.is_new)
+    make_read_aware(request_mock, post)
+    assert post.is_read
+    assert not post.is_new
 
-    def test_user_unread_post(self):
-        """tracked post is marked as unread for authenticated users"""
-        post = test.reply_thread(self.thread, posted_on=timezone.now())
 
-        make_read_aware(self.user, post)
-        self.assertFalse(post.is_read)
-        self.assertTrue(post.is_new)
+def test_tracked_read_post_is_marked_as_read_and_not_new(request_mock, read_post):
+    make_read_aware(request_mock, read_post)
+    assert read_post.is_read
+    assert not read_post.is_new
 
-    def test_user_created_after_post(self):
-        """tracked post older than user is marked as read"""
-        posted_on = timezone.now() - timedelta(days=1)
-        post = test.reply_thread(self.thread, posted_on=posted_on)
 
-        make_read_aware(self.user, post)
-        self.assertTrue(post.is_read)
-        self.assertFalse(post.is_new)
+@override_dynamic_settings(readtracker_cutoff=3)
+def test_not_tracked_read_post_is_marked_as_read_and_not_new(request_mock, read_post):
+    read_post.posted_on = timezone.now() - timedelta(days=4)
+    read_post.save()
 
-    def test_user_read_post(self):
-        """tracked post is marked as read for authenticated users with read entry"""
-        post = test.reply_thread(self.thread, posted_on=timezone.now())
+    make_read_aware(request_mock, read_post)
+    assert read_post.is_read
+    assert not read_post.is_new
 
-        save_read(self.user, post)
-        make_read_aware(self.user, post)
 
-        self.assertTrue(post.is_read)
-        self.assertFalse(post.is_new)
+def test_iterable_of_posts_can_be_made_read_aware(request_mock, post):
+    make_read_aware(request_mock, [post])
+    assert not post.is_read
+    assert post.is_new
+
+
+def test_tracked_post_read_by_other_user_is_marked_as_not_read_and_new(
+    request_mock, other_user, post
+):
+    save_read(other_user, post)
+    make_read_aware(request_mock, post)
+    assert not post.is_read
+    assert post.is_new
+
+
+def test_tracked_post_is_marked_as_read_and_not_new_for_anonymous_user(
+    anonymous_request_mock, post
+):
+    make_read_aware(anonymous_request_mock, post)
+    assert post.is_read
+    assert not post.is_new
+
+
+@override_dynamic_settings(readtracker_cutoff=3)
+def test_not_tracked_post_is_marked_as_read_and_not_new_for_anonymous_user(
+    anonymous_request_mock, post
+):
+    post.posted_on = timezone.now() - timedelta(days=4)
+    post.save()
+
+    make_read_aware(anonymous_request_mock, post)
+    assert post.is_read
+    assert not post.is_new

+ 87 - 131
misago/readtracker/tests/test_threadstracker.py

@@ -1,168 +1,124 @@
 from datetime import timedelta
 
-from django.test import TestCase
 from django.utils import timezone
 
-from .. import poststracker, threadstracker
-from ...acl.objectacl import add_acl_to_obj
-from ...acl.useracl import get_user_acl
-from ...categories.models import Category
-from ...conf import settings
-from ...conftest import get_cache_versions
-from ...threads import test
-from ...users.test import create_test_user
+from ...conf.test import override_dynamic_settings
+from ...threads.test import reply_thread
+from ..threadstracker import make_read_aware
 
-cache_versions = get_cache_versions()
 
+def remove_tracking(thread):
+    thread.started_on = timezone.now() - timedelta(days=4)
+    thread.save()
+    thread.first_post.posted_on = thread.started_on
+    thread.first_post.save()
 
-class AnonymousUser:
-    is_authenticated = False
-    is_anonymous = True
 
+def test_falsy_value_can_be_made_read_aware(request_mock):
+    make_read_aware(request_mock, None)
+    make_read_aware(request_mock, False)
 
-class ThreadsTrackerTests(TestCase):
-    def setUp(self):
-        self.user = create_test_user("User", "user@example.com")
-        self.user_acl = get_user_acl(self.user, cache_versions)
-        self.category = Category.objects.get(slug="first-category")
 
-        add_acl_to_obj(self.user_acl, self.category)
+def test_empty_list_can_be_made_read_aware(request_mock):
+    make_read_aware(request_mock, [])
 
-    def test_falsy_value(self):
-        """passing falsy value to readtracker causes no errors"""
-        threadstracker.make_read_aware(self.user, self.user_acl, None)
-        threadstracker.make_read_aware(self.user, self.user_acl, False)
-        threadstracker.make_read_aware(self.user, self.user_acl, [])
 
-    def test_anon_thread_before_cutoff(self):
-        """non-tracked thread is marked as read for anonymous users"""
-        started_on = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF)
-        thread = test.post_thread(self.category, started_on=started_on)
+def test_read_thread_is_marked_as_read(request_mock, read_thread):
+    make_read_aware(request_mock, read_thread)
+    assert read_thread.is_read
+    assert not read_thread.is_new
 
-        threadstracker.make_read_aware(AnonymousUser(), None, thread)
-        self.assertTrue(thread.is_read)
-        self.assertFalse(thread.is_new)
 
-    def test_anon_thread_after_cutoff(self):
-        """tracked thread is marked as read for anonymous users"""
-        thread = test.post_thread(self.category, started_on=timezone.now())
+def test_read_thread_with_hidden_post_marked_as_unread(request_mock, read_thread):
+    reply_thread(read_thread, is_hidden=True)
+    make_read_aware(request_mock, read_thread)
+    assert not read_thread.is_read
+    assert read_thread.is_new
 
-        threadstracker.make_read_aware(AnonymousUser(), None, thread)
-        self.assertTrue(thread.is_read)
-        self.assertFalse(thread.is_new)
 
-    def test_user_thread_before_cutoff(self):
-        """non-tracked thread is marked as read for authenticated users"""
-        started_on = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF)
-        thread = test.post_thread(self.category, started_on=started_on)
+def test_read_thread_with_invisible_post_marked_as_read(request_mock, read_thread):
+    reply_thread(read_thread, is_unapproved=True)
+    make_read_aware(request_mock, read_thread)
+    assert read_thread.is_read
+    assert not read_thread.is_new
 
-        threadstracker.make_read_aware(self.user, self.user_acl, thread)
-        self.assertTrue(thread.is_read)
-        self.assertFalse(thread.is_new)
 
-    def test_user_unread_thread(self):
-        """tracked thread is marked as unread for authenticated users"""
-        thread = test.post_thread(self.category, started_on=timezone.now())
+def test_read_thread_with_unread_post_marked_as_unread(request_mock, read_thread):
+    reply_thread(read_thread)
+    make_read_aware(request_mock, read_thread)
+    assert not read_thread.is_read
+    assert read_thread.is_new
 
-        threadstracker.make_read_aware(self.user, self.user_acl, thread)
-        self.assertFalse(thread.is_read)
-        self.assertTrue(thread.is_new)
 
-    def test_user_created_after_thread(self):
-        """tracked thread older than user is marked as read"""
-        started_on = timezone.now() - timedelta(days=1)
-        thread = test.post_thread(self.category, started_on=started_on)
+def test_untracked_thread_with_tracked_post_is_marked_as_unread(request_mock, thread):
+    remove_tracking(thread)
+    reply_thread(thread)
+    make_read_aware(request_mock, thread)
+    assert not thread.is_read
+    assert thread.is_new
 
-        threadstracker.make_read_aware(self.user, self.user_acl, thread)
-        self.assertTrue(thread.is_read)
-        self.assertFalse(thread.is_new)
 
-    def test_user_read_post(self):
-        """tracked thread with read post marked as read"""
-        thread = test.post_thread(self.category, started_on=timezone.now())
+def test_tracked_thread_is_marked_as_unread(request_mock, thread):
+    make_read_aware(request_mock, thread)
+    assert not thread.is_read
+    assert thread.is_new
 
-        poststracker.save_read(self.user, thread.first_post)
 
-        threadstracker.make_read_aware(self.user, self.user_acl, thread)
-        self.assertTrue(thread.is_read)
-        self.assertFalse(thread.is_new)
+def test_thread_with_post_older_than_user_is_marked_as_read(request_mock, thread, user):
+    remove_tracking(thread)
+    make_read_aware(request_mock, thread)
+    assert thread.is_read
+    assert not thread.is_new
 
-    def test_user_first_unread_last_read_post(self):
-        """tracked thread with unread first and last read post marked as unread"""
-        thread = test.post_thread(self.category, started_on=timezone.now())
 
-        post = test.reply_thread(thread, posted_on=timezone.now())
-        poststracker.save_read(self.user, post)
+@override_dynamic_settings(readtracker_cutoff=3)
+def test_non_tracked_thread_is_marked_as_read(request_mock, thread, user):
+    user.joined_on = timezone.now() - timedelta(days=10)
+    user.save()
 
-        threadstracker.make_read_aware(self.user, self.user_acl, thread)
-        self.assertFalse(thread.is_read)
-        self.assertTrue(thread.is_new)
+    remove_tracking(thread)
+    make_read_aware(request_mock, thread)
+    assert thread.is_read
+    assert not thread.is_new
 
-    def test_user_first_read_post_unread_event(self):
-        """tracked thread with read first post and unread event"""
-        thread = test.post_thread(self.category, started_on=timezone.now())
-        poststracker.save_read(self.user, thread.first_post)
 
-        test.reply_thread(thread, posted_on=timezone.now(), is_event=True)
+def test_read_thread_with_new_event_is_marked_as_unread(request_mock, read_thread):
+    reply_thread(read_thread, is_event=True)
+    make_read_aware(request_mock, read_thread)
+    assert not read_thread.is_read
+    assert read_thread.is_new
 
-        threadstracker.make_read_aware(self.user, self.user_acl, thread)
-        self.assertFalse(thread.is_read)
-        self.assertTrue(thread.is_new)
 
-    def test_user_hidden_event(self):
-        """tracked thread with unread first post and hidden event"""
-        thread = test.post_thread(self.category, started_on=timezone.now())
+def test_read_thread_with_hidden_event_is_marked_as_read(request_mock, read_thread):
+    reply_thread(read_thread, is_hidden=True, is_event=True)
+    make_read_aware(request_mock, read_thread)
+    assert read_thread.is_read
+    assert not read_thread.is_new
 
-        test.reply_thread(
-            thread, posted_on=timezone.now(), is_event=True, is_hidden=True
-        )
 
-        threadstracker.make_read_aware(self.user, self.user_acl, thread)
-        self.assertFalse(thread.is_read)
-        self.assertTrue(thread.is_new)
-
-    def test_user_first_read_post_hidden_event(self):
-        """tracked thread with read first post and hidden event"""
-        thread = test.post_thread(self.category, started_on=timezone.now())
-        poststracker.save_read(self.user, thread.first_post)
+def test_read_thread_with_hidden_event_visible_to_user_is_marked_as_unread(
+    request_mock, read_thread, default_category
+):
+    request_mock.user_acl["categories"][default_category.id]["can_hide_events"] = 1
+    reply_thread(read_thread, is_hidden=True, is_event=True)
+    make_read_aware(request_mock, read_thread)
+    assert not read_thread.is_read
+    assert read_thread.is_new
 
-        test.reply_thread(
-            thread, posted_on=timezone.now(), is_event=True, is_hidden=True
-        )
-
-        threadstracker.make_read_aware(self.user, self.user_acl, thread)
-        self.assertTrue(thread.is_read)
-        self.assertFalse(thread.is_new)
 
-    def test_user_thread_before_cutoff_unread_post(self):
-        """non-tracked thread is marked as unread for anonymous users"""
-        started_on = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF)
-        thread = test.post_thread(self.category, started_on=started_on)
+def test_tracked_thread_is_marked_as_read_for_anonymous_user(
+    anonymous_request_mock, thread
+):
+    make_read_aware(anonymous_request_mock, thread)
+    assert thread.is_read
+    assert not thread.is_new
 
-        threadstracker.make_read_aware(self.user, self.user_acl, thread)
-        self.assertTrue(thread.is_read)
-        self.assertFalse(thread.is_new)
 
-    def test_user_first_read_post_unapproved_post(self):
-        """tracked thread with read first post and unapproved post"""
-        thread = test.post_thread(self.category, started_on=timezone.now())
-        poststracker.save_read(self.user, thread.first_post)
-
-        test.reply_thread(thread, posted_on=timezone.now(), is_unapproved=True)
-
-        threadstracker.make_read_aware(self.user, self.user_acl, thread)
-        self.assertTrue(thread.is_read)
-        self.assertFalse(thread.is_new)
-
-    def test_user_first_read_post_unapproved_own_post(self):
-        """tracked thread with read first post and unapproved own post"""
-        thread = test.post_thread(self.category, started_on=timezone.now())
-        poststracker.save_read(self.user, thread.first_post)
-
-        test.reply_thread(
-            thread, posted_on=timezone.now(), poster=self.user, is_unapproved=True
-        )
-
-        threadstracker.make_read_aware(self.user, self.user_acl, thread)
-        self.assertFalse(thread.is_read)
-        self.assertTrue(thread.is_new)
+@override_dynamic_settings(readtracker_cutoff=3)
+def test_non_tracked_thread_is_marked_as_read_for_anonymous_user(
+    anonymous_request_mock, thread
+):
+    remove_tracking(thread)
+    make_read_aware(anonymous_request_mock, thread)
+    assert thread.is_read
+    assert not thread.is_new

+ 7 - 6
misago/readtracker/threadstracker.py

@@ -1,9 +1,9 @@
 from ..threads.models import Post
 from ..threads.permissions import exclude_invisible_posts
-from .dates import get_cutoff_date
+from .cutoffdate import get_cutoff_date
 
 
-def make_read_aware(user, user_acl, threads):
+def make_read_aware(request, threads):
     if not threads:
         return
 
@@ -12,19 +12,20 @@ def make_read_aware(user, user_acl, threads):
 
     make_read(threads)
 
-    if user.is_anonymous:
+    if request.user.is_anonymous:
         return
 
     categories = [t.category for t in threads]
+    cutoff_date = get_cutoff_date(request.settings, request.user)
 
     queryset = (
-        Post.objects.filter(thread__in=threads, posted_on__gt=get_cutoff_date(user))
+        Post.objects.filter(thread__in=threads, posted_on__gt=cutoff_date)
         .values_list("thread", flat=True)
         .distinct()
     )
 
-    queryset = queryset.exclude(id__in=user.postread_set.values("post"))
-    queryset = exclude_invisible_posts(user_acl, categories, queryset)
+    queryset = queryset.exclude(id__in=request.user.postread_set.values("post"))
+    queryset = exclude_invisible_posts(request.user_acl, categories, queryset)
 
     unread_threads = list(queryset)
 

+ 1 - 1
misago/static/misago/admin/index.css

@@ -1,2 +1,2 @@
-:root{--blue: #0052cc;--indigo: #6610f2;--purple: #6554c0;--pink: #e83e8c;--red: #ff5630;--orange: #ffab00;--yellow: #ffc107;--green: #36b37e;--teal: #20c997;--cyan: #00b8d9;--white: #fff;--gray: #a5adba;--gray-dark: #505f79;--primary: #6554c0;--secondary: #a5adba;--success: #36b37e;--info: #00b8d9;--warning: #ffc107;--danger: #ff5630;--light: #ebecf0;--dark: #505f79;--breakpoint-xs: 0;--breakpoint-sm: 576px;--breakpoint-md: 768px;--breakpoint-lg: 992px;--breakpoint-xl: 1200px;--font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}*,*::before,*::after{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(9,30,66,0)}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#172b4d;text-align:left;background-color:#f4f5f7}[tabindex="-1"]:focus{outline:0 !important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[title],abbr[data-original-title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul,dl{margin-top:0;margin-bottom:1rem}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#6b778c;text-decoration:none;background-color:transparent}a:hover{color:#172b4d;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):hover,a:not([href]):not([tabindex]):focus{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}pre,code,kbd,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#a5adba;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}input,button,select,optgroup,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}button,[type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button:not(:disabled),[type="button"]:not(:disabled),[type="reset"]:not(:disabled),[type="submit"]:not(:disabled){cursor:pointer}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{padding:0;border-style:none}input[type="radio"],input[type="checkbox"]{box-sizing:border-box;padding:0}input[type="date"],input[type="time"],input[type="datetime-local"],input[type="month"]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{outline-offset:-2px;-webkit-appearance:none}[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none !important}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{margin-bottom:.5rem;font-weight:500;line-height:1.2}h1,.h1{font-size:2.5rem}h2,.h2{font-size:2rem}h3,.h3{font-size:1.75rem}h4,.h4{font-size:1.5rem}h5,.h5{font-size:1.25rem}h6,.h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(9,30,66,0.1)}small,.small{font-size:80%;font-weight:400}mark,.mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#a5adba}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#f4f5f7;border:1px solid #dfe1e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#a5adba}code{font-size:87.5%;color:#e83e8c;word-break:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#172b4d;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#172b4d}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width: 576px){.container{max-width:540px}}@media (min-width: 768px){.container{max-width:720px}}@media (min-width: 992px){.container{max-width:960px}}@media (min-width: 1200px){.container{max-width:1140px}}.container-fluid{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*="col-"]{padding-right:0;padding-left:0}.col-1,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-10,.col-11,.col-12,.col,.col-auto,.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm,.col-sm-auto,.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12,.col-md,.col-md-auto,.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg,.col-lg-auto,.col-xl-1,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-ms-flex:0 0 8.33333%;flex:0 0 8.33333%;max-width:8.33333%}.col-2{-ms-flex:0 0 16.66667%;flex:0 0 16.66667%;max-width:16.66667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.33333%;flex:0 0 33.33333%;max-width:33.33333%}.col-5{-ms-flex:0 0 41.66667%;flex:0 0 41.66667%;max-width:41.66667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.33333%;flex:0 0 58.33333%;max-width:58.33333%}.col-8{-ms-flex:0 0 66.66667%;flex:0 0 66.66667%;max-width:66.66667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.33333%;flex:0 0 83.33333%;max-width:83.33333%}.col-11{-ms-flex:0 0 91.66667%;flex:0 0 91.66667%;max-width:91.66667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.33333%}.offset-2{margin-left:16.66667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333%}.offset-5{margin-left:41.66667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333%}.offset-8{margin-left:66.66667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333%}.offset-11{margin-left:91.66667%}@media (min-width: 576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-ms-flex:0 0 8.33333%;flex:0 0 8.33333%;max-width:8.33333%}.col-sm-2{-ms-flex:0 0 16.66667%;flex:0 0 16.66667%;max-width:16.66667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.33333%;flex:0 0 33.33333%;max-width:33.33333%}.col-sm-5{-ms-flex:0 0 41.66667%;flex:0 0 41.66667%;max-width:41.66667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.33333%;flex:0 0 58.33333%;max-width:58.33333%}.col-sm-8{-ms-flex:0 0 66.66667%;flex:0 0 66.66667%;max-width:66.66667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.33333%;flex:0 0 83.33333%;max-width:83.33333%}.col-sm-11{-ms-flex:0 0 91.66667%;flex:0 0 91.66667%;max-width:91.66667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333%}.offset-sm-2{margin-left:16.66667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333%}.offset-sm-5{margin-left:41.66667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333%}.offset-sm-8{margin-left:66.66667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333%}.offset-sm-11{margin-left:91.66667%}}@media (min-width: 768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-ms-flex:0 0 8.33333%;flex:0 0 8.33333%;max-width:8.33333%}.col-md-2{-ms-flex:0 0 16.66667%;flex:0 0 16.66667%;max-width:16.66667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.33333%;flex:0 0 33.33333%;max-width:33.33333%}.col-md-5{-ms-flex:0 0 41.66667%;flex:0 0 41.66667%;max-width:41.66667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.33333%;flex:0 0 58.33333%;max-width:58.33333%}.col-md-8{-ms-flex:0 0 66.66667%;flex:0 0 66.66667%;max-width:66.66667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.33333%;flex:0 0 83.33333%;max-width:83.33333%}.col-md-11{-ms-flex:0 0 91.66667%;flex:0 0 91.66667%;max-width:91.66667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333%}.offset-md-2{margin-left:16.66667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333%}.offset-md-5{margin-left:41.66667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333%}.offset-md-8{margin-left:66.66667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333%}.offset-md-11{margin-left:91.66667%}}@media (min-width: 992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-ms-flex:0 0 8.33333%;flex:0 0 8.33333%;max-width:8.33333%}.col-lg-2{-ms-flex:0 0 16.66667%;flex:0 0 16.66667%;max-width:16.66667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.33333%;flex:0 0 33.33333%;max-width:33.33333%}.col-lg-5{-ms-flex:0 0 41.66667%;flex:0 0 41.66667%;max-width:41.66667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.33333%;flex:0 0 58.33333%;max-width:58.33333%}.col-lg-8{-ms-flex:0 0 66.66667%;flex:0 0 66.66667%;max-width:66.66667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.33333%;flex:0 0 83.33333%;max-width:83.33333%}.col-lg-11{-ms-flex:0 0 91.66667%;flex:0 0 91.66667%;max-width:91.66667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333%}.offset-lg-2{margin-left:16.66667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333%}.offset-lg-5{margin-left:41.66667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333%}.offset-lg-8{margin-left:66.66667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333%}.offset-lg-11{margin-left:91.66667%}}@media (min-width: 1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-ms-flex:0 0 8.33333%;flex:0 0 8.33333%;max-width:8.33333%}.col-xl-2{-ms-flex:0 0 16.66667%;flex:0 0 16.66667%;max-width:16.66667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.33333%;flex:0 0 33.33333%;max-width:33.33333%}.col-xl-5{-ms-flex:0 0 41.66667%;flex:0 0 41.66667%;max-width:41.66667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.33333%;flex:0 0 58.33333%;max-width:58.33333%}.col-xl-8{-ms-flex:0 0 66.66667%;flex:0 0 66.66667%;max-width:66.66667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.33333%;flex:0 0 83.33333%;max-width:83.33333%}.col-xl-11{-ms-flex:0 0 91.66667%;flex:0 0 91.66667%;max-width:91.66667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333%}.offset-xl-2{margin-left:16.66667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333%}.offset-xl-5{margin-left:41.66667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333%}.offset-xl-8{margin-left:66.66667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333%}.offset-xl-11{margin-left:91.66667%}}.table{width:100%;margin-bottom:1rem;color:#172b4d}.table th,.table td{padding:.75rem;vertical-align:top;border-top:1px solid #dfe1e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dfe1e6}.table tbody+tbody{border-top:2px solid #dfe1e6}.table-sm th,.table-sm td{padding:.3rem}.table-bordered{border:1px solid #dfe1e6}.table-bordered th,.table-bordered td{border:1px solid #dfe1e6}.table-bordered thead th,.table-bordered thead td{border-bottom-width:2px}.table-borderless th,.table-borderless td,.table-borderless thead th,.table-borderless tbody+tbody{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(9,30,66,0.05)}.table-hover tbody tr:hover{color:#172b4d;background-color:rgba(9,30,66,0.075)}.table-primary,.table-primary>th,.table-primary>td{background-color:#d4cfed}.table-primary th,.table-primary td,.table-primary thead th,.table-primary tbody+tbody{border-color:#afa6de}.table-hover .table-primary:hover{background-color:#c3bce6}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#c3bce6}.table-secondary,.table-secondary>th,.table-secondary>td{background-color:#e6e8ec}.table-secondary th,.table-secondary td,.table-secondary thead th,.table-secondary tbody+tbody{border-color:#d0d4db}.table-hover .table-secondary:hover{background-color:#d8dbe1}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#d8dbe1}.table-success,.table-success>th,.table-success>td{background-color:#c7eadb}.table-success th,.table-success td,.table-success thead th,.table-success tbody+tbody{border-color:#96d7bc}.table-hover .table-success:hover{background-color:#b4e3cf}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b4e3cf}.table-info,.table-info>th,.table-info>td{background-color:#b8ebf4}.table-info th,.table-info td,.table-info thead th,.table-info tbody+tbody{border-color:#7adaeb}.table-hover .table-info:hover{background-color:#a2e5f1}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#a2e5f1}.table-warning,.table-warning>th,.table-warning>td{background-color:#ffeeba}.table-warning th,.table-warning td,.table-warning thead th,.table-warning tbody+tbody{border-color:#ffdf7e}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>th,.table-danger>td{background-color:#ffd0c5}.table-danger th,.table-danger td,.table-danger thead th,.table-danger tbody+tbody{border-color:#ffa793}.table-hover .table-danger:hover{background-color:#ffbbac}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#ffbbac}.table-light,.table-light>th,.table-light>td{background-color:#f9fafb}.table-light th,.table-light td,.table-light thead th,.table-light tbody+tbody{border-color:#f5f5f7}.table-hover .table-light:hover{background-color:#eaedf1}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#eaedf1}.table-dark,.table-dark>th,.table-dark>td{background-color:#ced2d9}.table-dark th,.table-dark td,.table-dark thead th,.table-dark tbody+tbody{border-color:#a4acb9}.table-hover .table-dark:hover{background-color:#c0c5ce}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#c0c5ce}.table-active,.table-active>th,.table-active>td{background-color:rgba(9,30,66,0.075)}.table-hover .table-active:hover{background-color:rgba(6,20,44,0.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(6,20,44,0.075)}.table .thead-dark th{color:#fff;background-color:#505f79;border-color:#5f7190}.table .thead-light th{color:#6b778c;background-color:rgba(0,0,0,0);border-color:#dfe1e6}.table-dark{color:#fff;background-color:#505f79}.table-dark th,.table-dark td,.table-dark thead th{border-color:#5f7190}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,0.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,0.075)}@media (max-width: 575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width: 767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width: 991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width: 1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#6b778c;background-color:#fff;background-clip:padding-box;border:1px solid #c1c7d0;border-radius:.25rem;transition:border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#6b778c;background-color:#fff;border-color:#1851b2;outline:0;box-shadow:0 0 0 .2rem rgba(0,82,204,0.15)}.form-control::-webkit-input-placeholder{color:#a5adba;opacity:1}.form-control::-moz-placeholder{color:#a5adba;opacity:1}.form-control:-ms-input-placeholder{color:#a5adba;opacity:1}.form-control::-ms-input-placeholder{color:#a5adba;opacity:1}.form-control::placeholder{color:#a5adba;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#ebecf0;opacity:1}select.form-control:focus::-ms-value{color:#6b778c;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding-top:.375rem;padding-bottom:.375rem;margin-bottom:0;line-height:1.5;color:#172b4d;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-sm,.form-control-plaintext.form-control-lg{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[size],select.form-control[multiple]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*="col-"]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled ~ .form-check-label{color:#a5adba}.form-check-label{margin-bottom:0}.form-check-inline{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#36b37e}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(54,179,126,0.9);border-radius:.25rem}.was-validated .form-control:valid,.form-control.is-valid{border-color:#36b37e;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2336b37e' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:center right calc(.375em + .1875rem);background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.was-validated .form-control:valid:focus,.form-control.is-valid:focus{border-color:#36b37e;box-shadow:0 0 0 .2rem rgba(54,179,126,0.25)}.was-validated .form-control:valid ~ .valid-feedback,.was-validated .form-control:valid ~ .valid-tooltip,.form-control.is-valid ~ .valid-feedback,.form-control.is-valid ~ .valid-tooltip{display:block}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.was-validated .custom-select:valid,.custom-select.is-valid{border-color:#36b37e;padding-right:calc((1em + .75rem) * 3 / 4 + 1.75rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23505f79' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2336b37e' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.was-validated .custom-select:valid:focus,.custom-select.is-valid:focus{border-color:#36b37e;box-shadow:0 0 0 .2rem rgba(54,179,126,0.25)}.was-validated .custom-select:valid ~ .valid-feedback,.was-validated .custom-select:valid ~ .valid-tooltip,.custom-select.is-valid ~ .valid-feedback,.custom-select.is-valid ~ .valid-tooltip{display:block}.was-validated .form-control-file:valid ~ .valid-feedback,.was-validated .form-control-file:valid ~ .valid-tooltip,.form-control-file.is-valid ~ .valid-feedback,.form-control-file.is-valid ~ .valid-tooltip{display:block}.was-validated .form-check-input:valid ~ .form-check-label,.form-check-input.is-valid ~ .form-check-label{color:#36b37e}.was-validated .form-check-input:valid ~ .valid-feedback,.was-validated .form-check-input:valid ~ .valid-tooltip,.form-check-input.is-valid ~ .valid-feedback,.form-check-input.is-valid ~ .valid-tooltip{display:block}.was-validated .custom-control-input:valid ~ .custom-control-label,.custom-control-input.is-valid ~ .custom-control-label{color:#36b37e}.was-validated .custom-control-input:valid ~ .custom-control-label::before,.custom-control-input.is-valid ~ .custom-control-label::before{border-color:#36b37e}.was-validated .custom-control-input:valid ~ .valid-feedback,.was-validated .custom-control-input:valid ~ .valid-tooltip,.custom-control-input.is-valid ~ .valid-feedback,.custom-control-input.is-valid ~ .valid-tooltip{display:block}.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before,.custom-control-input.is-valid:checked ~ .custom-control-label::before{border-color:#51cb97;background-color:#51cb97}.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before,.custom-control-input.is-valid:focus ~ .custom-control-label::before{box-shadow:0 0 0 .2rem rgba(54,179,126,0.25)}.was-validated .custom-control-input:valid:focus:not(:checked) ~ .custom-control-label::before,.custom-control-input.is-valid:focus:not(:checked) ~ .custom-control-label::before{border-color:#36b37e}.was-validated .custom-file-input:valid ~ .custom-file-label,.custom-file-input.is-valid ~ .custom-file-label{border-color:#36b37e}.was-validated .custom-file-input:valid ~ .valid-feedback,.was-validated .custom-file-input:valid ~ .valid-tooltip,.custom-file-input.is-valid ~ .valid-feedback,.custom-file-input.is-valid ~ .valid-tooltip{display:block}.was-validated .custom-file-input:valid:focus ~ .custom-file-label,.custom-file-input.is-valid:focus ~ .custom-file-label{border-color:#36b37e;box-shadow:0 0 0 .2rem rgba(54,179,126,0.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#ff5630}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(255,86,48,0.9);border-radius:.25rem}.was-validated .form-control:invalid,.form-control.is-invalid{border-color:#ff5630;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23ff5630' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23ff5630' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E");background-repeat:no-repeat;background-position:center right calc(.375em + .1875rem);background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.was-validated .form-control:invalid:focus,.form-control.is-invalid:focus{border-color:#ff5630;box-shadow:0 0 0 .2rem rgba(255,86,48,0.25)}.was-validated .form-control:invalid ~ .invalid-feedback,.was-validated .form-control:invalid ~ .invalid-tooltip,.form-control.is-invalid ~ .invalid-feedback,.form-control.is-invalid ~ .invalid-tooltip{display:block}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.was-validated .custom-select:invalid,.custom-select.is-invalid{border-color:#ff5630;padding-right:calc((1em + .75rem) * 3 / 4 + 1.75rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23505f79' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23ff5630' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23ff5630' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.was-validated .custom-select:invalid:focus,.custom-select.is-invalid:focus{border-color:#ff5630;box-shadow:0 0 0 .2rem rgba(255,86,48,0.25)}.was-validated .custom-select:invalid ~ .invalid-feedback,.was-validated .custom-select:invalid ~ .invalid-tooltip,.custom-select.is-invalid ~ .invalid-feedback,.custom-select.is-invalid ~ .invalid-tooltip{display:block}.was-validated .form-control-file:invalid ~ .invalid-feedback,.was-validated .form-control-file:invalid ~ .invalid-tooltip,.form-control-file.is-invalid ~ .invalid-feedback,.form-control-file.is-invalid ~ .invalid-tooltip{display:block}.was-validated .form-check-input:invalid ~ .form-check-label,.form-check-input.is-invalid ~ .form-check-label{color:#ff5630}.was-validated .form-check-input:invalid ~ .invalid-feedback,.was-validated .form-check-input:invalid ~ .invalid-tooltip,.form-check-input.is-invalid ~ .invalid-feedback,.form-check-input.is-invalid ~ .invalid-tooltip{display:block}.was-validated .custom-control-input:invalid ~ .custom-control-label,.custom-control-input.is-invalid ~ .custom-control-label{color:#ff5630}.was-validated .custom-control-input:invalid ~ .custom-control-label::before,.custom-control-input.is-invalid ~ .custom-control-label::before{border-color:#ff5630}.was-validated .custom-control-input:invalid ~ .invalid-feedback,.was-validated .custom-control-input:invalid ~ .invalid-tooltip,.custom-control-input.is-invalid ~ .invalid-feedback,.custom-control-input.is-invalid ~ .invalid-tooltip{display:block}.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before,.custom-control-input.is-invalid:checked ~ .custom-control-label::before{border-color:#ff8063;background-color:#ff8063}.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before,.custom-control-input.is-invalid:focus ~ .custom-control-label::before{box-shadow:0 0 0 .2rem rgba(255,86,48,0.25)}.was-validated .custom-control-input:invalid:focus:not(:checked) ~ .custom-control-label::before,.custom-control-input.is-invalid:focus:not(:checked) ~ .custom-control-label::before{border-color:#ff5630}.was-validated .custom-file-input:invalid ~ .custom-file-label,.custom-file-input.is-invalid ~ .custom-file-label{border-color:#ff5630}.was-validated .custom-file-input:invalid ~ .invalid-feedback,.was-validated .custom-file-input:invalid ~ .invalid-tooltip,.custom-file-input.is-invalid ~ .invalid-feedback,.custom-file-input.is-invalid ~ .invalid-tooltip{display:block}.was-validated .custom-file-input:invalid:focus ~ .custom-file-label,.custom-file-input.is-invalid:focus ~ .custom-file-label{border-color:#ff5630;box-shadow:0 0 0 .2rem rgba(255,86,48,0.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width: 576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .input-group,.form-inline .custom-select{width:auto}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;-ms-flex-negative:0;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#172b4d;text-align:center;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.btn{transition:none}}.btn:hover{color:#172b4d;text-decoration:none}.btn:focus,.btn.focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,82,204,0.15)}.btn.disabled,.btn:disabled{opacity:.65}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#6554c0;border-color:#6554c0}.btn-primary:hover{color:#fff;background-color:#5140ae;border-color:#4d3da4}.btn-primary:focus,.btn-primary.focus{box-shadow:0 0 0 .2rem rgba(124,110,201,0.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#6554c0;border-color:#6554c0}.btn-primary:not(:disabled):not(.disabled):active,.btn-primary:not(:disabled):not(.disabled).active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#4d3da4;border-color:#49399b}.btn-primary:not(:disabled):not(.disabled):active:focus,.btn-primary:not(:disabled):not(.disabled).active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(124,110,201,0.5)}.btn-secondary{color:#172b4d;background-color:#a5adba;border-color:#a5adba}.btn-secondary:hover{color:#172b4d;background-color:#8f99a9;border-color:#8893a4}.btn-secondary:focus,.btn-secondary.focus{box-shadow:0 0 0 .2rem rgba(144,154,170,0.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#172b4d;background-color:#a5adba;border-color:#a5adba}.btn-secondary:not(:disabled):not(.disabled):active,.btn-secondary:not(:disabled):not(.disabled).active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#8893a4;border-color:#818c9e}.btn-secondary:not(:disabled):not(.disabled):active:focus,.btn-secondary:not(:disabled):not(.disabled).active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(144,154,170,0.5)}.btn-success{color:#fff;background-color:#36b37e;border-color:#36b37e}.btn-success:hover{color:#fff;background-color:#2d9669;border-color:#2a8c62}.btn-success:focus,.btn-success.focus{box-shadow:0 0 0 .2rem rgba(84,190,145,0.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#36b37e;border-color:#36b37e}.btn-success:not(:disabled):not(.disabled):active,.btn-success:not(:disabled):not(.disabled).active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#2a8c62;border-color:#27825c}.btn-success:not(:disabled):not(.disabled):active:focus,.btn-success:not(:disabled):not(.disabled).active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(84,190,145,0.5)}.btn-info{color:#fff;background-color:#00b8d9;border-color:#00b8d9}.btn-info:hover{color:#fff;background-color:#0098b3;border-color:#008da6}.btn-info:focus,.btn-info.focus{box-shadow:0 0 0 .2rem rgba(38,195,223,0.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#00b8d9;border-color:#00b8d9}.btn-info:not(:disabled):not(.disabled):active,.btn-info:not(:disabled):not(.disabled).active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#008da6;border-color:#008299}.btn-info:not(:disabled):not(.disabled):active:focus,.btn-info:not(:disabled):not(.disabled).active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(38,195,223,0.5)}.btn-warning{color:#172b4d;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#172b4d;background-color:#e0a800;border-color:#d39e00}.btn-warning:focus,.btn-warning.focus{box-shadow:0 0 0 .2rem rgba(220,171,18,0.5)}.btn-warning.disabled,.btn-warning:disabled{color:#172b4d;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled):active,.btn-warning:not(:disabled):not(.disabled).active,.show>.btn-warning.dropdown-toggle{color:#172b4d;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled):active:focus,.btn-warning:not(:disabled):not(.disabled).active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,171,18,0.5)}.btn-danger{color:#fff;background-color:#ff5630;border-color:#ff5630}.btn-danger:hover{color:#fff;background-color:#ff370a;border-color:#fc2e00}.btn-danger:focus,.btn-danger.focus{box-shadow:0 0 0 .2rem rgba(255,111,79,0.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#ff5630;border-color:#ff5630}.btn-danger:not(:disabled):not(.disabled):active,.btn-danger:not(:disabled):not(.disabled).active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#fc2e00;border-color:#ef2c00}.btn-danger:not(:disabled):not(.disabled):active:focus,.btn-danger:not(:disabled):not(.disabled).active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,111,79,0.5)}.btn-light,.navbar .btn-user,.card-admin-table .btn-thumbnail{color:#172b4d;background-color:#ebecf0;border-color:#ebecf0}.btn-light:hover,.navbar .btn-user:hover,.card-admin-table .btn-thumbnail:hover{color:#172b4d;background-color:#d5d7e0;border-color:#ced0da}.btn-light:focus,.navbar .btn-user:focus,.card-admin-table .btn-thumbnail:focus,.btn-light.focus,.navbar .focus.btn-user,.card-admin-table .focus.btn-thumbnail{box-shadow:0 0 0 .2rem rgba(203,207,216,0.5)}.btn-light.disabled,.navbar .disabled.btn-user,.card-admin-table .disabled.btn-thumbnail,.btn-light:disabled,.navbar .btn-user:disabled,.card-admin-table .btn-thumbnail:disabled{color:#172b4d;background-color:#ebecf0;border-color:#ebecf0}.btn-light:not(:disabled):not(.disabled):active,.navbar .btn-user:not(:disabled):not(.disabled):active,.card-admin-table .btn-thumbnail:not(:disabled):not(.disabled):active,.btn-light:not(:disabled):not(.disabled).active,.navbar .btn-user:not(:disabled):not(.disabled).active,.card-admin-table .btn-thumbnail:not(:disabled):not(.disabled).active,.show>.btn-light.dropdown-toggle,.navbar .show>.dropdown-toggle.btn-user,.card-admin-table .show>.dropdown-toggle.btn-thumbnail{color:#172b4d;background-color:#ced0da;border-color:#c7c9d5}.btn-light:not(:disabled):not(.disabled):active:focus,.navbar .btn-user:not(:disabled):not(.disabled):active:focus,.card-admin-table .btn-thumbnail:not(:disabled):not(.disabled):active:focus,.btn-light:not(:disabled):not(.disabled).active:focus,.navbar .btn-user:not(:disabled):not(.disabled).active:focus,.card-admin-table .btn-thumbnail:not(:disabled):not(.disabled).active:focus,.show>.btn-light.dropdown-toggle:focus,.navbar .show>.dropdown-toggle.btn-user:focus,.card-admin-table .show>.dropdown-toggle.btn-thumbnail:focus{box-shadow:0 0 0 .2rem rgba(203,207,216,0.5)}.btn-dark{color:#fff;background-color:#505f79;border-color:#505f79}.btn-dark:hover{color:#fff;background-color:#414d62;border-color:#3c475a}.btn-dark:focus,.btn-dark.focus{box-shadow:0 0 0 .2rem rgba(106,119,141,0.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#505f79;border-color:#505f79}.btn-dark:not(:disabled):not(.disabled):active,.btn-dark:not(:disabled):not(.disabled).active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#3c475a;border-color:#374153}.btn-dark:not(:disabled):not(.disabled):active:focus,.btn-dark:not(:disabled):not(.disabled).active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(106,119,141,0.5)}.btn-outline-primary{color:#6554c0;border-color:#6554c0}.btn-outline-primary:hover{color:#fff;background-color:#6554c0;border-color:#6554c0}.btn-outline-primary:focus,.btn-outline-primary.focus{box-shadow:0 0 0 .2rem rgba(101,84,192,0.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#6554c0;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled):active,.btn-outline-primary:not(:disabled):not(.disabled).active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#6554c0;border-color:#6554c0}.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(101,84,192,0.5)}.btn-outline-secondary{color:#a5adba;border-color:#a5adba}.btn-outline-secondary:hover{color:#172b4d;background-color:#a5adba;border-color:#a5adba}.btn-outline-secondary:focus,.btn-outline-secondary.focus{box-shadow:0 0 0 .2rem rgba(165,173,186,0.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#a5adba;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled):active,.btn-outline-secondary:not(:disabled):not(.disabled).active,.show>.btn-outline-secondary.dropdown-toggle{color:#172b4d;background-color:#a5adba;border-color:#a5adba}.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(165,173,186,0.5)}.btn-outline-success{color:#36b37e;border-color:#36b37e}.btn-outline-success:hover{color:#fff;background-color:#36b37e;border-color:#36b37e}.btn-outline-success:focus,.btn-outline-success.focus{box-shadow:0 0 0 .2rem rgba(54,179,126,0.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#36b37e;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled):active,.btn-outline-success:not(:disabled):not(.disabled).active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#36b37e;border-color:#36b37e}.btn-outline-success:not(:disabled):not(.disabled):active:focus,.btn-outline-success:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(54,179,126,0.5)}.btn-outline-info{color:#00b8d9;border-color:#00b8d9}.btn-outline-info:hover{color:#fff;background-color:#00b8d9;border-color:#00b8d9}.btn-outline-info:focus,.btn-outline-info.focus{box-shadow:0 0 0 .2rem rgba(0,184,217,0.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#00b8d9;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled):active,.btn-outline-info:not(:disabled):not(.disabled).active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#00b8d9;border-color:#00b8d9}.btn-outline-info:not(:disabled):not(.disabled):active:focus,.btn-outline-info:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,184,217,0.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#172b4d;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:focus,.btn-outline-warning.focus{box-shadow:0 0 0 .2rem rgba(255,193,7,0.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled):active,.btn-outline-warning:not(:disabled):not(.disabled).active,.show>.btn-outline-warning.dropdown-toggle{color:#172b4d;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,0.5)}.btn-outline-danger{color:#ff5630;border-color:#ff5630}.btn-outline-danger:hover{color:#fff;background-color:#ff5630;border-color:#ff5630}.btn-outline-danger:focus,.btn-outline-danger.focus{box-shadow:0 0 0 .2rem rgba(255,86,48,0.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#ff5630;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled):active,.btn-outline-danger:not(:disabled):not(.disabled).active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#ff5630;border-color:#ff5630}.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,86,48,0.5)}.btn-outline-light{color:#ebecf0;border-color:#ebecf0}.btn-outline-light:hover{color:#172b4d;background-color:#ebecf0;border-color:#ebecf0}.btn-outline-light:focus,.btn-outline-light.focus{box-shadow:0 0 0 .2rem rgba(235,236,240,0.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#ebecf0;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled):active,.btn-outline-light:not(:disabled):not(.disabled).active,.show>.btn-outline-light.dropdown-toggle{color:#172b4d;background-color:#ebecf0;border-color:#ebecf0}.btn-outline-light:not(:disabled):not(.disabled):active:focus,.btn-outline-light:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(235,236,240,0.5)}.btn-outline-dark{color:#505f79;border-color:#505f79}.btn-outline-dark:hover{color:#fff;background-color:#505f79;border-color:#505f79}.btn-outline-dark:focus,.btn-outline-dark.focus{box-shadow:0 0 0 .2rem rgba(80,95,121,0.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#505f79;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled):active,.btn-outline-dark:not(:disabled):not(.disabled).active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#505f79;border-color:#505f79}.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(80,95,121,0.5)}.btn-link{font-weight:400;color:#6b778c;text-decoration:none}.btn-link:hover{color:#172b4d;text-decoration:underline}.btn-link:focus,.btn-link.focus{text-decoration:underline;box-shadow:none}.btn-link:disabled,.btn-link.disabled{color:#a5adba;pointer-events:none}.btn-lg,.btn-group-lg>.btn{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-sm,.btn-group-sm>.btn{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block,.card-admin-table .btn-thumbnail{display:block;width:100%}.btn-block+.btn-block,.card-admin-table .btn-thumbnail+.btn-block,.card-admin-table .btn-block+.btn-thumbnail,.card-admin-table .btn-thumbnail+.btn-thumbnail{margin-top:.5rem}input[type="submit"].btn-block,.card-admin-table input.btn-thumbnail[type="submit"],input[type="reset"].btn-block,.card-admin-table input.btn-thumbnail[type="reset"],input[type="button"].btn-block,.card-admin-table input.btn-thumbnail[type="button"]{width:100%}.fade{transition:opacity 0.15s linear}@media (prefers-reduced-motion: reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height 0.35s ease}@media (prefers-reduced-motion: reduce){.collapsing{transition:none}}.dropup,.dropright,.dropdown,.dropleft{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#172b4d;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(9,30,66,0.15);border-radius:.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width: 576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width: 768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width: 992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width: 1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^="top"],.dropdown-menu[x-placement^="right"],.dropdown-menu[x-placement^="bottom"],.dropdown-menu[x-placement^="left"]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #ebecf0}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#172b4d;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:hover,.dropdown-item:focus{color:#112039;text-decoration:none;background-color:#f4f5f7}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#091e42}.dropdown-item.disabled,.dropdown-item:disabled{color:#a5adba;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#a5adba;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#172b4d}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover{z-index:1}.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn.active{z-index:1}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:not(:first-child),.btn-group>.btn-group:not(:first-child){margin-left:-1px}.btn-group>.btn:not(:last-child):not(.dropdown-toggle),.btn-group>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:not(:first-child),.btn-group>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-sm+.dropdown-toggle-split,.btn-group-sm>.btn+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-lg+.dropdown-toggle-split,.btn-group-lg>.btn+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle),.btn-group-vertical>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type="radio"],.btn-group-toggle>.btn input[type="checkbox"],.btn-group-toggle>.btn-group>.btn input[type="radio"],.btn-group-toggle>.btn-group>.btn input[type="checkbox"]{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-control-plaintext,.input-group>.custom-select,.input-group>.custom-file{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;margin-bottom:0}.input-group>.form-control+.form-control,.input-group>.form-control+.custom-select,.input-group>.form-control+.custom-file,.input-group>.form-control-plaintext+.form-control,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.custom-file,.input-group>.custom-select+.form-control,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.custom-file,.input-group>.custom-file+.form-control,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.custom-file{margin-left:-1px}.input-group>.form-control:focus,.input-group>.custom-select:focus,.input-group>.custom-file .custom-file-input:focus ~ .custom-file-label{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.form-control:not(:last-child),.input-group>.custom-select:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.form-control:not(:first-child),.input-group>.custom-select:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-prepend,.input-group-append{display:-ms-flexbox;display:flex}.input-group-prepend .btn,.input-group-append .btn{position:relative;z-index:2}.input-group-prepend .btn:focus,.input-group-append .btn:focus{z-index:3}.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.input-group-text,.input-group-append .input-group-text+.btn{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#6b778c;text-align:center;white-space:nowrap;background-color:#ebecf0;border:1px solid #c1c7d0;border-radius:.25rem}.input-group-text input[type="radio"],.input-group-text input[type="checkbox"]{margin-top:0}.input-group-lg>.form-control:not(textarea),.input-group-lg>.custom-select{height:calc(1.5em + 1rem + 2px)}.input-group-lg>.form-control,.input-group-lg>.custom-select,.input-group-lg>.input-group-prepend>.input-group-text,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-append>.btn{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.form-control:not(textarea),.input-group-sm>.custom-select{height:calc(1.5em + .5rem + 2px)}.input-group-sm>.form-control,.input-group-sm>.custom-select,.input-group-sm>.input-group-prepend>.input-group-text,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-append>.btn{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text,.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked ~ .custom-control-label::before{color:#fff;border-color:#091e42;background-color:#091e42}.custom-control-input:focus ~ .custom-control-label::before{box-shadow:0 0 0 .2rem rgba(0,82,204,0.15)}.custom-control-input:focus:not(:checked) ~ .custom-control-label::before{border-color:#1851b2}.custom-control-input:not(:disabled):active ~ .custom-control-label::before{color:#fff;background-color:#1e65df;border-color:#1e65df}.custom-control-input:disabled ~ .custom-control-label{color:#a5adba}.custom-control-input:disabled ~ .custom-control-label::before{background-color:#ebecf0}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#b3bac5 solid 1px}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:no-repeat 50% / 50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before{border-color:#091e42;background-color:#091e42}.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before{background-color:rgba(101,84,192,0.5)}.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before{background-color:rgba(101,84,192,0.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked ~ .custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before{background-color:rgba(101,84,192,0.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#b3bac5;border-radius:.5rem;transition:background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out,-webkit-transform 0.15s ease-in-out;transition:transform 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out;transition:transform 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out,-webkit-transform 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked ~ .custom-control-label::after{background-color:#fff;-webkit-transform:translateX(.75rem);transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before{background-color:rgba(101,84,192,0.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#6b778c;vertical-align:middle;background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23505f79' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px;background-color:#fff;border:1px solid #c1c7d0;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#1851b2;outline:0;box-shadow:0 0 0 .2rem rgba(0,82,204,0.15)}.custom-select:focus::-ms-value{color:#6b778c;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#a5adba;background-color:#ebecf0}.custom-select::-ms-expand{display:none}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + .75rem + 2px);margin:0;opacity:0}.custom-file-input:focus ~ .custom-file-label{border-color:#1851b2;box-shadow:0 0 0 .2rem rgba(0,82,204,0.15)}.custom-file-input:disabled ~ .custom-file-label{background-color:#ebecf0}.custom-file-input:lang(en) ~ .custom-file-label::after{content:"Browse"}.custom-file-input ~ .custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-weight:400;line-height:1.5;color:#6b778c;background-color:#fff;border:1px solid #c1c7d0;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);padding:.375rem .75rem;line-height:1.5;color:#6b778c;content:"Browse";background-color:#ebecf0;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:calc(1rem + .4rem);padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:none}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #f4f5f7,0 0 0 .2rem rgba(0,82,204,0.15)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #f4f5f7,0 0 0 .2rem rgba(0,82,204,0.15)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #f4f5f7,0 0 0 .2rem rgba(0,82,204,0.15)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#091e42;border:0;border-radius:1rem;transition:background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-webkit-slider-thumb{transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#1e65df}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dfe1e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#091e42;border:0;border-radius:1rem;transition:background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-moz-range-thumb{transition:none}}.custom-range::-moz-range-thumb:active{background-color:#1e65df}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dfe1e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#091e42;border:0;border-radius:1rem;transition:background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out;appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-ms-thumb{transition:none}}.custom-range::-ms-thumb:active{background-color:#1e65df}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dfe1e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dfe1e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#b3bac5}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#b3bac5}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#b3bac5}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:hover,.nav-link:focus{text-decoration:none}.nav-link.disabled{color:#a5adba;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dfe1e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:hover,.nav-tabs .nav-link:focus{border-color:#ebecf0 #ebecf0 #dfe1e6}.nav-tabs .nav-link.disabled{color:#a5adba;background-color:transparent;border-color:transparent}.nav-tabs .nav-link.active,.nav-tabs .nav-item.show .nav-link{color:#6b778c;background-color:#f4f5f7;border-color:#dfe1e6 #dfe1e6 #f4f5f7}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#091e42}.nav-fill .nav-item{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar>.container,.navbar>.container-fluid{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:hover,.navbar-toggler:focus{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width: 575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{padding-right:0;padding-left:0}}@media (min-width: 576px){.navbar-expand-sm{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width: 767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{padding-right:0;padding-left:0}}@media (min-width: 768px){.navbar-expand-md{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width: 991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{padding-right:0;padding-left:0}}@media (min-width: 992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width: 1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{padding-right:0;padding-left:0}}@media (min-width: 1200px){.navbar-expand-xl{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(9,30,66,0.9)}.navbar-light .navbar-brand:hover,.navbar-light .navbar-brand:focus{color:rgba(9,30,66,0.9)}.navbar-light .navbar-nav .nav-link{color:rgba(9,30,66,0.5)}.navbar-light .navbar-nav .nav-link:hover,.navbar-light .navbar-nav .nav-link:focus{color:rgba(9,30,66,0.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(9,30,66,0.3)}.navbar-light .navbar-nav .show>.nav-link,.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .nav-link.active{color:rgba(9,30,66,0.9)}.navbar-light .navbar-toggler{color:rgba(9,30,66,0.5);border-color:rgba(9,30,66,0.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(9,30,66,0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(9,30,66,0.5)}.navbar-light .navbar-text a{color:rgba(9,30,66,0.9)}.navbar-light .navbar-text a:hover,.navbar-light .navbar-text a:focus{color:rgba(9,30,66,0.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:hover,.navbar-dark .navbar-brand:focus{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,0.5)}.navbar-dark .navbar-nav .nav-link:hover,.navbar-dark .navbar-nav .nav-link:focus{color:rgba(255,255,255,0.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,0.25)}.navbar-dark .navbar-nav .show>.nav-link,.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .nav-link.active{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,0.5);border-color:rgba(255,255,255,0.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(255,255,255,0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,0.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:hover,.navbar-dark .navbar-text a:focus{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(9,30,66,0.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-body,.card-admin-form .form-fieldset{-ms-flex:1 1 auto;flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,0);border-bottom:1px solid rgba(9,30,66,0.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,0);border-top:1px solid rgba(9,30,66,0.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img{width:100%;border-radius:calc(.25rem - 1px)}.card-img-top{width:100%;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img-bottom{width:100%;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-deck .card{margin-bottom:15px}@media (min-width: 576px){.card-deck{-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{display:-ms-flexbox;display:flex;-ms-flex:1 0 0%;flex:1 0 0%;-ms-flex-direction:column;flex-direction:column;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-group>.card{margin-bottom:15px}@media (min-width: 576px){.card-group{-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-img-top,.card-group>.card:not(:last-child) .card-header{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-img-bottom,.card-group>.card:not(:last-child) .card-footer{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-img-top,.card-group>.card:not(:first-child) .card-header{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-img-bottom,.card-group>.card:not(:first-child) .card-footer{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width: 576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion>.card{overflow:hidden}.accordion>.card:not(:first-of-type) .card-header:first-child{border-radius:0}.accordion>.card:not(:first-of-type):not(:last-of-type){border-bottom:0;border-radius:0}.accordion>.card:first-of-type{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:last-of-type{border-top-left-radius:0;border-top-right-radius:0}.accordion>.card .card-header{margin-bottom:-1px}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#ebecf0;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;color:#a5adba;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#a5adba}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#6b778c;background-color:#fff;border:1px solid #dfe1e6}.page-link:hover{z-index:2;color:#172b4d;text-decoration:none;background-color:#ebecf0;border-color:#dfe1e6}.page-link:focus{z-index:2;outline:0;box-shadow:0 0 0 .2rem rgba(0,82,204,0.15)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:1;color:#fff;background-color:#091e42;border-color:#091e42}.page-item.disabled .page-link{color:#a5adba;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dfe1e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.badge{transition:none}}a.badge:hover,a.badge:focus{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#6554c0}a.badge-primary:hover,a.badge-primary:focus{color:#fff;background-color:#4d3da4}a.badge-primary:focus,a.badge-primary.focus{outline:0;box-shadow:0 0 0 .2rem rgba(101,84,192,0.5)}.badge-secondary{color:#172b4d;background-color:#a5adba}a.badge-secondary:hover,a.badge-secondary:focus{color:#172b4d;background-color:#8893a4}a.badge-secondary:focus,a.badge-secondary.focus{outline:0;box-shadow:0 0 0 .2rem rgba(165,173,186,0.5)}.badge-success{color:#fff;background-color:#36b37e}a.badge-success:hover,a.badge-success:focus{color:#fff;background-color:#2a8c62}a.badge-success:focus,a.badge-success.focus{outline:0;box-shadow:0 0 0 .2rem rgba(54,179,126,0.5)}.badge-info{color:#fff;background-color:#00b8d9}a.badge-info:hover,a.badge-info:focus{color:#fff;background-color:#008da6}a.badge-info:focus,a.badge-info.focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,184,217,0.5)}.badge-warning{color:#172b4d;background-color:#ffc107}a.badge-warning:hover,a.badge-warning:focus{color:#172b4d;background-color:#d39e00}a.badge-warning:focus,a.badge-warning.focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,193,7,0.5)}.badge-danger{color:#fff;background-color:#ff5630}a.badge-danger:hover,a.badge-danger:focus{color:#fff;background-color:#fc2e00}a.badge-danger:focus,a.badge-danger.focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,86,48,0.5)}.badge-light{color:#172b4d;background-color:#ebecf0}a.badge-light:hover,a.badge-light:focus{color:#172b4d;background-color:#ced0da}a.badge-light:focus,a.badge-light.focus{outline:0;box-shadow:0 0 0 .2rem rgba(235,236,240,0.5)}.badge-dark{color:#fff;background-color:#505f79}a.badge-dark:hover,a.badge-dark:focus{color:#fff;background-color:#3c475a}a.badge-dark:focus,a.badge-dark.focus{outline:0;box-shadow:0 0 0 .2rem rgba(80,95,121,0.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#ebecf0;border-radius:.3rem}@media (min-width: 576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:0 solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#2a316f;background-color:#e0ddf2;border-color:#d4cfed}.alert-primary hr{border-top-color:#c3bce6}.alert-primary .alert-link{color:#1c214a}.alert-secondary{color:#41516d;background-color:#edeff1;border-color:#e6e8ec}.alert-secondary hr{border-top-color:#d8dbe1}.alert-secondary .alert-link{color:#2e394d}.alert-success{color:#195458;background-color:#d7f0e5;border-color:#c7eadb}.alert-success hr{border-top-color:#b4e3cf}.alert-success .alert-link{color:#0e2e30}.alert-info{color:#065578;background-color:#ccf1f7;border-color:#b8ebf4}.alert-info hr{border-top-color:#a2e5f1}.alert-info .alert-link{color:#043347}.alert-warning{color:#62592d;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#3f391d}.alert-danger{color:#62323c;background-color:#ffddd6;border-color:#ffd0c5}.alert-danger hr{border-top-color:#ffbbac}.alert-danger .alert-link{color:#402127}.alert-light{color:#5a6881;background-color:#fbfbfc;border-color:#f9fafb}.alert-light hr{border-top-color:#eaedf1}.alert-light .alert-link{color:#455063}.alert-dark{color:#233556;background-color:#dcdfe4;border-color:#ced2d9}.alert-dark hr{border-top-color:#c0c5ce}.alert-dark .alert-link{color:#141f32}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#ebecf0;border-radius:.25rem}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;color:#fff;text-align:center;white-space:nowrap;background-color:#6554c0;transition:width 0.6s ease}@media (prefers-reduced-motion: reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion: reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media,.nav-side .nav-link{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#6b778c;text-align:inherit}.list-group-item-action:hover,.list-group-item-action:focus{z-index:1;color:#6b778c;text-decoration:none;background-color:#f4f5f7}.list-group-item-action:active{color:#172b4d;background-color:#ebecf0}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid rgba(9,30,66,0.125)}.list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item.disabled,.list-group-item:disabled{color:#a5adba;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#091e42;border-color:#091e42}.list-group-horizontal{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}@media (min-width: 576px){.list-group-horizontal-sm{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-sm .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}@media (min-width: 768px){.list-group-horizontal-md{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-md .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}@media (min-width: 992px){.list-group-horizontal-lg{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-lg .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}@media (min-width: 1200px){.list-group-horizontal-xl{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-xl .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush .list-group-item:last-child{margin-bottom:-1px}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{margin-bottom:0;border-bottom:0}.list-group-item-primary{color:#393a84;background-color:#d4cfed}.list-group-item-primary.list-group-item-action:hover,.list-group-item-primary.list-group-item-action:focus{color:#393a84;background-color:#c3bce6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#393a84;border-color:#393a84}.list-group-item-secondary{color:#5a6880;background-color:#e6e8ec}.list-group-item-secondary.list-group-item-action:hover,.list-group-item-secondary.list-group-item-action:focus{color:#5a6880;background-color:#d8dbe1}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#5a6880;border-color:#5a6880}.list-group-item-success{color:#206b61;background-color:#c7eadb}.list-group-item-success.list-group-item-action:hover,.list-group-item-success.list-group-item-action:focus{color:#206b61;background-color:#b4e3cf}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#206b61;border-color:#206b61}.list-group-item-info{color:#046e91;background-color:#b8ebf4}.list-group-item-info.list-group-item-action:hover,.list-group-item-info.list-group-item-action:focus{color:#046e91;background-color:#a2e5f1}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#046e91;border-color:#046e91}.list-group-item-warning{color:#897323;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:hover,.list-group-item-warning.list-group-item-action:focus{color:#897323;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#897323;border-color:#897323}.list-group-item-danger{color:#893b39;background-color:#ffd0c5}.list-group-item-danger.list-group-item-action:hover,.list-group-item-danger.list-group-item-action:focus{color:#893b39;background-color:#ffbbac}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#893b39;border-color:#893b39}.list-group-item-light{color:#7f899c;background-color:#f9fafb}.list-group-item-light.list-group-item-action:hover,.list-group-item-light.list-group-item-action:focus{color:#7f899c;background-color:#eaedf1}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#7f899c;border-color:#7f899c}.list-group-item-dark{color:#2e405f;background-color:#ced2d9}.list-group-item-dark.list-group-item-action:hover,.list-group-item-dark.list-group-item-action:focus{color:#2e405f;background-color:#c0c5ce}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#2e405f;border-color:#2e405f}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#091e42;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover{color:#091e42;text-decoration:none}.close:not(:disabled):not(.disabled):hover,.close:not(:disabled):not(.disabled):focus{opacity:.75}button.close{padding:0;background-color:transparent;border:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}a.close.disabled{pointer-events:none}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform 0.3s ease-out;transition:transform 0.3s ease-out;transition:transform 0.3s ease-out, -webkit-transform 0.3s ease-out;-webkit-transform:translate(0, -50px);transform:translate(0, -50px)}@media (prefers-reduced-motion: reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal-dialog-scrollable{display:-ms-flexbox;display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-header,.modal-dialog-scrollable .modal-footer{-ms-flex-negative:0;flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);content:""}.modal-dialog-centered.modal-dialog-scrollable{-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(9,30,66,0.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#091e42}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dfe1e6;border-top-left-radius:.3rem;border-top-right-radius:.3rem}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:1rem;border-top:1px solid #dfe1e6;border-bottom-right-radius:.3rem;border-bottom-left-radius:.3rem}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width: 576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width: 992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width: 1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-top,.bs-tooltip-auto[x-placement^="top"]{padding:.4rem 0}.bs-tooltip-top .arrow,.bs-tooltip-auto[x-placement^="top"] .arrow{bottom:0}.bs-tooltip-top .arrow::before,.bs-tooltip-auto[x-placement^="top"] .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#091e42}.bs-tooltip-right,.bs-tooltip-auto[x-placement^="right"]{padding:0 .4rem}.bs-tooltip-right .arrow,.bs-tooltip-auto[x-placement^="right"] .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-right .arrow::before,.bs-tooltip-auto[x-placement^="right"] .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#091e42}.bs-tooltip-bottom,.bs-tooltip-auto[x-placement^="bottom"]{padding:.4rem 0}.bs-tooltip-bottom .arrow,.bs-tooltip-auto[x-placement^="bottom"] .arrow{top:0}.bs-tooltip-bottom .arrow::before,.bs-tooltip-auto[x-placement^="bottom"] .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#091e42}.bs-tooltip-left,.bs-tooltip-auto[x-placement^="left"]{padding:0 .4rem}.bs-tooltip-left .arrow,.bs-tooltip-auto[x-placement^="left"] .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-left .arrow::before,.bs-tooltip-auto[x-placement^="left"] .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#091e42}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#091e42;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(9,30,66,0.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::before,.popover .arrow::after{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-top,.bs-popover-auto[x-placement^="top"]{margin-bottom:.5rem}.bs-popover-top>.arrow,.bs-popover-auto[x-placement^="top"]>.arrow{bottom:calc((.5rem + 1px) * -1)}.bs-popover-top>.arrow::before,.bs-popover-auto[x-placement^="top"]>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(9,30,66,0.25)}.bs-popover-top>.arrow::after,.bs-popover-auto[x-placement^="top"]>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-right,.bs-popover-auto[x-placement^="right"]{margin-left:.5rem}.bs-popover-right>.arrow,.bs-popover-auto[x-placement^="right"]>.arrow{left:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-right>.arrow::before,.bs-popover-auto[x-placement^="right"]>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(9,30,66,0.25)}.bs-popover-right>.arrow::after,.bs-popover-auto[x-placement^="right"]>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-bottom,.bs-popover-auto[x-placement^="bottom"]{margin-top:.5rem}.bs-popover-bottom>.arrow,.bs-popover-auto[x-placement^="bottom"]>.arrow{top:calc((.5rem + 1px) * -1)}.bs-popover-bottom>.arrow::before,.bs-popover-auto[x-placement^="bottom"]>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(9,30,66,0.25)}.bs-popover-bottom>.arrow::after,.bs-popover-auto[x-placement^="bottom"]>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-bottom .popover-header::before,.bs-popover-auto[x-placement^="bottom"] .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-left,.bs-popover-auto[x-placement^="left"]{margin-right:.5rem}.bs-popover-left>.arrow,.bs-popover-auto[x-placement^="left"]>.arrow{right:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-left>.arrow::before,.bs-popover-auto[x-placement^="left"]>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(9,30,66,0.25)}.bs-popover-left>.arrow::after,.bs-popover-auto[x-placement^="left"]>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#172b4d}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:spinner-border .75s linear infinite;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:spinner-grow .75s linear infinite;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}.align-baseline{vertical-align:baseline !important}.align-top{vertical-align:top !important}.align-middle{vertical-align:middle !important}.align-bottom{vertical-align:bottom !important}.align-text-bottom{vertical-align:text-bottom !important}.align-text-top{vertical-align:text-top !important}.bg-primary{background-color:#6554c0 !important}a.bg-primary:hover,a.bg-primary:focus,button.bg-primary:hover,button.bg-primary:focus{background-color:#4d3da4 !important}.bg-secondary{background-color:#a5adba !important}a.bg-secondary:hover,a.bg-secondary:focus,button.bg-secondary:hover,button.bg-secondary:focus{background-color:#8893a4 !important}.bg-success{background-color:#36b37e !important}a.bg-success:hover,a.bg-success:focus,button.bg-success:hover,button.bg-success:focus{background-color:#2a8c62 !important}.bg-info{background-color:#00b8d9 !important}a.bg-info:hover,a.bg-info:focus,button.bg-info:hover,button.bg-info:focus{background-color:#008da6 !important}.bg-warning{background-color:#ffc107 !important}a.bg-warning:hover,a.bg-warning:focus,button.bg-warning:hover,button.bg-warning:focus{background-color:#d39e00 !important}.bg-danger{background-color:#ff5630 !important}a.bg-danger:hover,a.bg-danger:focus,button.bg-danger:hover,button.bg-danger:focus{background-color:#fc2e00 !important}.bg-light{background-color:#ebecf0 !important}a.bg-light:hover,a.bg-light:focus,button.bg-light:hover,button.bg-light:focus{background-color:#ced0da !important}.bg-dark{background-color:#505f79 !important}a.bg-dark:hover,a.bg-dark:focus,button.bg-dark:hover,button.bg-dark:focus{background-color:#3c475a !important}.bg-white{background-color:#fff !important}.bg-transparent{background-color:transparent !important}.border,.control-month-picker,.control-time-picker,.control-image-preview{border:1px solid #dfe1e6 !important}.border-top,.card-admin-form .form-fieldset+.form-fieldset{border-top:1px solid #dfe1e6 !important}.border-right{border-right:1px solid #dfe1e6 !important}.border-bottom{border-bottom:1px solid #dfe1e6 !important}.border-left,.page-header h1 small{border-left:1px solid #dfe1e6 !important}.border-0,.control-time-picker input{border:0 !important}.border-top-0,.card-admin-table>:first-child th{border-top:0 !important}.border-right-0{border-right:0 !important}.border-bottom-0,.card-admin-table th{border-bottom:0 !important}.border-left-0{border-left:0 !important}.border-primary{border-color:#6554c0 !important}.border-secondary{border-color:#a5adba !important}.border-success{border-color:#36b37e !important}.border-info{border-color:#00b8d9 !important}.border-warning{border-color:#ffc107 !important}.border-danger,.card-admin-error,.card-admin-error .card-header,.login-error-card{border-color:#ff5630 !important}.border-light{border-color:#ebecf0 !important}.border-dark{border-color:#505f79 !important}.border-white{border-color:#fff !important}.rounded-sm{border-radius:.2rem !important}.rounded,.control-month-picker,.control-time-picker,.control-image-preview,.card-admin-settings-card,.media-check-icon{border-radius:.25rem !important}.rounded-top{border-top-left-radius:.25rem !important;border-top-right-radius:.25rem !important}.rounded-right{border-top-right-radius:.25rem !important;border-bottom-right-radius:.25rem !important}.rounded-bottom{border-bottom-right-radius:.25rem !important;border-bottom-left-radius:.25rem !important}.rounded-left{border-top-left-radius:.25rem !important;border-bottom-left-radius:.25rem !important}.rounded-lg,.card-admin-table img{border-radius:.3rem !important}.rounded-circle{border-radius:50% !important}.rounded-pill{border-radius:50rem !important}.rounded-0{border-radius:0 !important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none !important}.d-inline{display:inline !important}.d-inline-block,.control-image-preview{display:inline-block !important}.d-block,.card-admin-settings-card{display:block !important}.d-table{display:table !important}.d-table-row{display:table-row !important}.d-table-cell{display:table-cell !important}.d-flex,.control-time-picker,.page-header h1,.media-check-icon{display:-ms-flexbox !important;display:flex !important}.d-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}@media (min-width: 576px){.d-sm-none{display:none !important}.d-sm-inline{display:inline !important}.d-sm-inline-block{display:inline-block !important}.d-sm-block{display:block !important}.d-sm-table{display:table !important}.d-sm-table-row{display:table-row !important}.d-sm-table-cell{display:table-cell !important}.d-sm-flex{display:-ms-flexbox !important;display:flex !important}.d-sm-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media (min-width: 768px){.d-md-none{display:none !important}.d-md-inline{display:inline !important}.d-md-inline-block{display:inline-block !important}.d-md-block{display:block !important}.d-md-table{display:table !important}.d-md-table-row{display:table-row !important}.d-md-table-cell{display:table-cell !important}.d-md-flex{display:-ms-flexbox !important;display:flex !important}.d-md-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media (min-width: 992px){.d-lg-none{display:none !important}.d-lg-inline{display:inline !important}.d-lg-inline-block{display:inline-block !important}.d-lg-block{display:block !important}.d-lg-table{display:table !important}.d-lg-table-row{display:table-row !important}.d-lg-table-cell{display:table-cell !important}.d-lg-flex{display:-ms-flexbox !important;display:flex !important}.d-lg-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media (min-width: 1200px){.d-xl-none{display:none !important}.d-xl-inline{display:inline !important}.d-xl-inline-block{display:inline-block !important}.d-xl-block{display:block !important}.d-xl-table{display:table !important}.d-xl-table-row{display:table-row !important}.d-xl-table-cell{display:table-cell !important}.d-xl-flex{display:-ms-flexbox !important;display:flex !important}.d-xl-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media print{.d-print-none{display:none !important}.d-print-inline{display:inline !important}.d-print-inline-block{display:inline-block !important}.d-print-block{display:block !important}.d-print-table{display:table !important}.d-print-table-row{display:table-row !important}.d-print-table-cell{display:table-cell !important}.d-print-flex{display:-ms-flexbox !important;display:flex !important}.d-print-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.85714%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row !important;flex-direction:row !important}.flex-column,.nav-side{-ms-flex-direction:column !important;flex-direction:column !important}.flex-row-reverse{-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-column-reverse{-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-fill{-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-grow-0{-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-grow-1{-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-start{-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-end{-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-center,.media-check-icon{-ms-flex-pack:center !important;justify-content:center !important}.justify-content-between{-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-start{-ms-flex-align:start !important;align-items:flex-start !important}.align-items-end{-ms-flex-align:end !important;align-items:flex-end !important}.align-items-center,.control-time-picker,.page-header h1,.media-check-icon{-ms-flex-align:center !important;align-items:center !important}.align-items-baseline{-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-stretch{-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}@media (min-width: 576px){.flex-sm-row{-ms-flex-direction:row !important;flex-direction:row !important}.flex-sm-column{-ms-flex-direction:column !important;flex-direction:column !important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-sm-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-sm-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-sm-fill{-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-sm-grow-0{-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-sm-grow-1{-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-sm-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-sm-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-sm-start{-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-sm-end{-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-sm-center{-ms-flex-pack:center !important;justify-content:center !important}.justify-content-sm-between{-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-sm-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-sm-start{-ms-flex-align:start !important;align-items:flex-start !important}.align-items-sm-end{-ms-flex-align:end !important;align-items:flex-end !important}.align-items-sm-center{-ms-flex-align:center !important;align-items:center !important}.align-items-sm-baseline{-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-sm-stretch{-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-sm-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-sm-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-sm-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-sm-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-sm-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-sm-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-sm-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-sm-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-sm-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-sm-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-sm-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-sm-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}@media (min-width: 768px){.flex-md-row{-ms-flex-direction:row !important;flex-direction:row !important}.flex-md-column{-ms-flex-direction:column !important;flex-direction:column !important}.flex-md-row-reverse{-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-md-column-reverse{-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-md-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-md-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-md-fill{-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-md-grow-0{-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-md-grow-1{-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-md-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-md-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-md-start{-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-md-end{-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-md-center{-ms-flex-pack:center !important;justify-content:center !important}.justify-content-md-between{-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-md-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-md-start{-ms-flex-align:start !important;align-items:flex-start !important}.align-items-md-end{-ms-flex-align:end !important;align-items:flex-end !important}.align-items-md-center{-ms-flex-align:center !important;align-items:center !important}.align-items-md-baseline{-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-md-stretch{-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-md-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-md-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-md-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-md-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-md-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-md-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-md-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-md-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-md-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-md-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-md-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-md-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}@media (min-width: 992px){.flex-lg-row{-ms-flex-direction:row !important;flex-direction:row !important}.flex-lg-column{-ms-flex-direction:column !important;flex-direction:column !important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-lg-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-lg-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-lg-fill{-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-lg-grow-0{-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-lg-grow-1{-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-lg-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-lg-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-lg-start{-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-lg-end{-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-lg-center{-ms-flex-pack:center !important;justify-content:center !important}.justify-content-lg-between{-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-lg-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-lg-start{-ms-flex-align:start !important;align-items:flex-start !important}.align-items-lg-end{-ms-flex-align:end !important;align-items:flex-end !important}.align-items-lg-center{-ms-flex-align:center !important;align-items:center !important}.align-items-lg-baseline{-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-lg-stretch{-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-lg-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-lg-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-lg-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-lg-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-lg-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-lg-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-lg-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-lg-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-lg-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-lg-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-lg-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-lg-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}@media (min-width: 1200px){.flex-xl-row{-ms-flex-direction:row !important;flex-direction:row !important}.flex-xl-column{-ms-flex-direction:column !important;flex-direction:column !important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-xl-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-xl-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-xl-fill{-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-xl-grow-0{-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-xl-grow-1{-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-xl-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-xl-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-xl-start{-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-xl-end{-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-xl-center{-ms-flex-pack:center !important;justify-content:center !important}.justify-content-xl-between{-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-xl-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-xl-start{-ms-flex-align:start !important;align-items:flex-start !important}.align-items-xl-end{-ms-flex-align:end !important;align-items:flex-end !important}.align-items-xl-center{-ms-flex-align:center !important;align-items:center !important}.align-items-xl-baseline{-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-xl-stretch{-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-xl-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-xl-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-xl-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-xl-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-xl-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-xl-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-xl-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-xl-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-xl-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-xl-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-xl-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-xl-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}.float-left{float:left !important}.float-right{float:right !important}.float-none{float:none !important}@media (min-width: 576px){.float-sm-left{float:left !important}.float-sm-right{float:right !important}.float-sm-none{float:none !important}}@media (min-width: 768px){.float-md-left{float:left !important}.float-md-right{float:right !important}.float-md-none{float:none !important}}@media (min-width: 992px){.float-lg-left{float:left !important}.float-lg-right{float:right !important}.float-lg-none{float:none !important}}@media (min-width: 1200px){.float-xl-left{float:left !important}.float-xl-right{float:right !important}.float-xl-none{float:none !important}}.overflow-auto{overflow:auto !important}.overflow-hidden{overflow:hidden !important}.position-static{position:static !important}.position-relative{position:relative !important}.position-absolute{position:absolute !important}.position-fixed{position:fixed !important}.position-sticky{position:-webkit-sticky !important;position:sticky !important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position: -webkit-sticky) or (position: sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm,.login-form-card,.card-admin-settings-grid,.card-admin-info,.card-admin-table,.card-admin-form,.card-admin-error,.login-error-card{box-shadow:0 0.125rem 0.25rem rgba(9,30,66,0.075) !important}.shadow{box-shadow:0 0.5rem 1rem rgba(9,30,66,0.15) !important}.shadow-lg{box-shadow:0 1rem 3rem rgba(9,30,66,0.175) !important}.shadow-none{box-shadow:none !important}.w-25{width:25% !important}.w-50{width:50% !important}.w-75{width:75% !important}.w-100,.card-admin-table .row-select label{width:100% !important}.w-auto{width:auto !important}.h-25{height:25% !important}.h-50{height:50% !important}.h-75{height:75% !important}.h-100{height:100% !important}.h-auto{height:auto !important}.mw-100{max-width:100% !important}.mh-100{max-height:100% !important}.min-vw-100{min-width:100vw !important}.min-vh-100{min-height:100vh !important}.vw-100{width:100vw !important}.vh-100{height:100vh !important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.m-0,.page-header h1,.card-admin-settings-card h5,.card-admin-info .card-title,.card-admin-table .card-title,.card-admin-table table,.card-admin-table h5,.card-admin-table .row-select label{margin:0 !important}.mt-0,.media-admin-check h5,.my-0{margin-top:0 !important}.mr-0,.mx-0{margin-right:0 !important}.mb-0,.card-admin-form .form-group:last-child,.card-admin-form .form-fieldset .form-group:last-child,.my-0{margin-bottom:0 !important}.ml-0,.mx-0{margin-left:0 !important}.m-1,.card-admin-settings-card{margin:.25rem !important}.mt-1,.control-checkboxselect .form-check+.form-check,.control-radioselect .form-check+.form-check,.control-image-preview div,.my-1{margin-top:.25rem !important}.mr-1,.page-action .fa,.page-action .fab,.mx-1{margin-right:.25rem !important}.mb-1,.my-1{margin-bottom:.25rem !important}.ml-1,.mx-1{margin-left:.25rem !important}.m-2{margin:.5rem !important}.mt-2,.my-2{margin-top:.5rem !important}.mr-2,.mx-2{margin-right:.5rem !important}.mb-2,.control-image-preview,.my-2{margin-bottom:.5rem !important}.ml-2,.control-image-preview div span+span,.mx-2{margin-left:.5rem !important}.m-3{margin:1rem !important}.mt-3,.my-3{margin-top:1rem !important}.mr-3,.media-check-icon,.mx-3{margin-right:1rem !important}.mb-3,.page-header,.card-admin-info,.card-admin-form .card-admin-table,.my-3{margin-bottom:1rem !important}.ml-3,.control-yesno-switch .radio+.radio,.page-header h1 small,.mx-3{margin-left:1rem !important}.m-4{margin:1.5rem !important}.mt-4,.my-4{margin-top:1.5rem !important}.mr-4,.mx-4{margin-right:1.5rem !important}.mb-4,.my-4{margin-bottom:1.5rem !important}.ml-4,.mx-4{margin-left:1.5rem !important}.m-5{margin:3rem !important}.mt-5,.my-5{margin-top:3rem !important}.mr-5,.mx-5{margin-right:3rem !important}.mb-5,.my-5{margin-bottom:3rem !important}.ml-5,.mx-5{margin-left:3rem !important}.p-0,.navbar .btn-user{padding:0 !important}.pt-0,.card-admin-form .form-fieldset:first-child,.py-0{padding-top:0 !important}.pr-0,.px-0{padding-right:0 !important}.pb-0,.control-image-preview.control-image-metadata,.card-admin-form .form-fieldset:last-child,.py-0{padding-bottom:0 !important}.pl-0,.card-admin-table .badges-list,.px-0{padding-left:0 !important}.p-1,.control-month-picker,.control-time-picker{padding:.25rem !important}.pt-1,.py-1,.page-header h1 small,.card-admin-settings-grid .card-body,.card-admin-settings-grid .card-admin-form .form-fieldset,.card-admin-form .card-admin-settings-grid .form-fieldset,.card-admin-table th,.card-admin-table td{padding-top:.25rem !important}.pr-1,.px-1{padding-right:.25rem !important}.pb-1,.py-1,.page-header h1 small,.card-admin-settings-grid .card-body,.card-admin-settings-grid .card-admin-form .form-fieldset,.card-admin-form .card-admin-settings-grid .form-fieldset,.card-admin-table th,.card-admin-table td{padding-bottom:.25rem !important}.pl-1,.nav-side .media-body,.px-1{padding-left:.25rem !important}.p-2,.card-admin-settings-card,.card-admin-table .row-select label{padding:.5rem !important}.pt-2,.py-2,.control-image-preview div{padding-top:.5rem !important}.pr-2,.px-2{padding-right:.5rem !important}.pb-2,.py-2,.control-image-preview div{padding-bottom:.5rem !important}.pl-2,.px-2{padding-left:.5rem !important}.p-3,.control-image-preview{padding:1rem !important}.pt-3,.py-3,.card-admin-analytics-summary,.card-admin-table .blankslate td{padding-top:1rem !important}.pr-3,.px-3{padding-right:1rem !important}.pb-3,.py-3,.card-admin-analytics-summary,.card-admin-table .blankslate td{padding-bottom:1rem !important}.pl-3,.page-header h1 small,.px-3{padding-left:1rem !important}.p-4{padding:1.5rem !important}.pt-4,.py-4{padding-top:1.5rem !important}.pr-4,.px-4{padding-right:1.5rem !important}.pb-4,.py-4{padding-bottom:1.5rem !important}.pl-4,.px-4{padding-left:1.5rem !important}.p-5{padding:3rem !important}.pt-5,.py-5{padding-top:3rem !important}.pr-5,.px-5{padding-right:3rem !important}.pb-5,.py-5{padding-bottom:3rem !important}.pl-5,.px-5{padding-left:3rem !important}.m-n1{margin:-.25rem !important}.mt-n1,.my-n1{margin-top:-.25rem !important}.mr-n1,.mx-n1{margin-right:-.25rem !important}.mb-n1,.my-n1{margin-bottom:-.25rem !important}.ml-n1,.mx-n1{margin-left:-.25rem !important}.m-n2{margin:-.5rem !important}.mt-n2,.my-n2{margin-top:-.5rem !important}.mr-n2,.mx-n2{margin-right:-.5rem !important}.mb-n2,.my-n2{margin-bottom:-.5rem !important}.ml-n2,.mx-n2{margin-left:-.5rem !important}.m-n3{margin:-1rem !important}.mt-n3,.my-n3{margin-top:-1rem !important}.mr-n3,.mx-n3{margin-right:-1rem !important}.mb-n3,.my-n3{margin-bottom:-1rem !important}.ml-n3,.mx-n3{margin-left:-1rem !important}.m-n4{margin:-1.5rem !important}.mt-n4,.my-n4{margin-top:-1.5rem !important}.mr-n4,.mx-n4{margin-right:-1.5rem !important}.mb-n4,.my-n4{margin-bottom:-1.5rem !important}.ml-n4,.mx-n4{margin-left:-1.5rem !important}.m-n5{margin:-3rem !important}.mt-n5,.my-n5{margin-top:-3rem !important}.mr-n5,.mx-n5{margin-right:-3rem !important}.mb-n5,.my-n5{margin-bottom:-3rem !important}.ml-n5,.mx-n5{margin-left:-3rem !important}.m-auto{margin:auto !important}.mt-auto,.my-auto{margin-top:auto !important}.mr-auto,.mx-auto{margin-right:auto !important}.mb-auto,.my-auto{margin-bottom:auto !important}.ml-auto,.mx-auto{margin-left:auto !important}@media (min-width: 576px){.m-sm-0{margin:0 !important}.mt-sm-0,.my-sm-0{margin-top:0 !important}.mr-sm-0,.mx-sm-0{margin-right:0 !important}.mb-sm-0,.my-sm-0{margin-bottom:0 !important}.ml-sm-0,.mx-sm-0{margin-left:0 !important}.m-sm-1{margin:.25rem !important}.mt-sm-1,.my-sm-1{margin-top:.25rem !important}.mr-sm-1,.mx-sm-1{margin-right:.25rem !important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem !important}.ml-sm-1,.mx-sm-1{margin-left:.25rem !important}.m-sm-2{margin:.5rem !important}.mt-sm-2,.my-sm-2{margin-top:.5rem !important}.mr-sm-2,.mx-sm-2{margin-right:.5rem !important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem !important}.ml-sm-2,.mx-sm-2{margin-left:.5rem !important}.m-sm-3{margin:1rem !important}.mt-sm-3,.my-sm-3{margin-top:1rem !important}.mr-sm-3,.mx-sm-3{margin-right:1rem !important}.mb-sm-3,.my-sm-3{margin-bottom:1rem !important}.ml-sm-3,.mx-sm-3{margin-left:1rem !important}.m-sm-4{margin:1.5rem !important}.mt-sm-4,.my-sm-4{margin-top:1.5rem !important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem !important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem !important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem !important}.m-sm-5{margin:3rem !important}.mt-sm-5,.my-sm-5{margin-top:3rem !important}.mr-sm-5,.mx-sm-5{margin-right:3rem !important}.mb-sm-5,.my-sm-5{margin-bottom:3rem !important}.ml-sm-5,.mx-sm-5{margin-left:3rem !important}.p-sm-0{padding:0 !important}.pt-sm-0,.py-sm-0{padding-top:0 !important}.pr-sm-0,.px-sm-0{padding-right:0 !important}.pb-sm-0,.py-sm-0{padding-bottom:0 !important}.pl-sm-0,.px-sm-0{padding-left:0 !important}.p-sm-1{padding:.25rem !important}.pt-sm-1,.py-sm-1{padding-top:.25rem !important}.pr-sm-1,.px-sm-1{padding-right:.25rem !important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem !important}.pl-sm-1,.px-sm-1{padding-left:.25rem !important}.p-sm-2{padding:.5rem !important}.pt-sm-2,.py-sm-2{padding-top:.5rem !important}.pr-sm-2,.px-sm-2{padding-right:.5rem !important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem !important}.pl-sm-2,.px-sm-2{padding-left:.5rem !important}.p-sm-3{padding:1rem !important}.pt-sm-3,.py-sm-3{padding-top:1rem !important}.pr-sm-3,.px-sm-3{padding-right:1rem !important}.pb-sm-3,.py-sm-3{padding-bottom:1rem !important}.pl-sm-3,.px-sm-3{padding-left:1rem !important}.p-sm-4{padding:1.5rem !important}.pt-sm-4,.py-sm-4{padding-top:1.5rem !important}.pr-sm-4,.px-sm-4{padding-right:1.5rem !important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem !important}.pl-sm-4,.px-sm-4{padding-left:1.5rem !important}.p-sm-5{padding:3rem !important}.pt-sm-5,.py-sm-5{padding-top:3rem !important}.pr-sm-5,.px-sm-5{padding-right:3rem !important}.pb-sm-5,.py-sm-5{padding-bottom:3rem !important}.pl-sm-5,.px-sm-5{padding-left:3rem !important}.m-sm-n1{margin:-.25rem !important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem !important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem !important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem !important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem !important}.m-sm-n2{margin:-.5rem !important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem !important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem !important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem !important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem !important}.m-sm-n3{margin:-1rem !important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem !important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem !important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem !important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem !important}.m-sm-n4{margin:-1.5rem !important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem !important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem !important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem !important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem !important}.m-sm-n5{margin:-3rem !important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem !important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem !important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem !important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem !important}.m-sm-auto{margin:auto !important}.mt-sm-auto,.my-sm-auto{margin-top:auto !important}.mr-sm-auto,.mx-sm-auto{margin-right:auto !important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto !important}.ml-sm-auto,.mx-sm-auto{margin-left:auto !important}}@media (min-width: 768px){.m-md-0{margin:0 !important}.mt-md-0,.my-md-0{margin-top:0 !important}.mr-md-0,.mx-md-0{margin-right:0 !important}.mb-md-0,.my-md-0{margin-bottom:0 !important}.ml-md-0,.mx-md-0{margin-left:0 !important}.m-md-1{margin:.25rem !important}.mt-md-1,.my-md-1{margin-top:.25rem !important}.mr-md-1,.mx-md-1{margin-right:.25rem !important}.mb-md-1,.my-md-1{margin-bottom:.25rem !important}.ml-md-1,.mx-md-1{margin-left:.25rem !important}.m-md-2{margin:.5rem !important}.mt-md-2,.my-md-2{margin-top:.5rem !important}.mr-md-2,.mx-md-2{margin-right:.5rem !important}.mb-md-2,.my-md-2{margin-bottom:.5rem !important}.ml-md-2,.mx-md-2{margin-left:.5rem !important}.m-md-3{margin:1rem !important}.mt-md-3,.my-md-3{margin-top:1rem !important}.mr-md-3,.mx-md-3{margin-right:1rem !important}.mb-md-3,.my-md-3{margin-bottom:1rem !important}.ml-md-3,.mx-md-3{margin-left:1rem !important}.m-md-4{margin:1.5rem !important}.mt-md-4,.my-md-4{margin-top:1.5rem !important}.mr-md-4,.mx-md-4{margin-right:1.5rem !important}.mb-md-4,.my-md-4{margin-bottom:1.5rem !important}.ml-md-4,.mx-md-4{margin-left:1.5rem !important}.m-md-5{margin:3rem !important}.mt-md-5,.my-md-5{margin-top:3rem !important}.mr-md-5,.mx-md-5{margin-right:3rem !important}.mb-md-5,.my-md-5{margin-bottom:3rem !important}.ml-md-5,.mx-md-5{margin-left:3rem !important}.p-md-0{padding:0 !important}.pt-md-0,.py-md-0{padding-top:0 !important}.pr-md-0,.px-md-0{padding-right:0 !important}.pb-md-0,.py-md-0{padding-bottom:0 !important}.pl-md-0,.px-md-0{padding-left:0 !important}.p-md-1{padding:.25rem !important}.pt-md-1,.py-md-1{padding-top:.25rem !important}.pr-md-1,.px-md-1{padding-right:.25rem !important}.pb-md-1,.py-md-1{padding-bottom:.25rem !important}.pl-md-1,.px-md-1{padding-left:.25rem !important}.p-md-2{padding:.5rem !important}.pt-md-2,.py-md-2{padding-top:.5rem !important}.pr-md-2,.px-md-2{padding-right:.5rem !important}.pb-md-2,.py-md-2{padding-bottom:.5rem !important}.pl-md-2,.px-md-2{padding-left:.5rem !important}.p-md-3{padding:1rem !important}.pt-md-3,.py-md-3{padding-top:1rem !important}.pr-md-3,.px-md-3{padding-right:1rem !important}.pb-md-3,.py-md-3{padding-bottom:1rem !important}.pl-md-3,.px-md-3{padding-left:1rem !important}.p-md-4{padding:1.5rem !important}.pt-md-4,.py-md-4{padding-top:1.5rem !important}.pr-md-4,.px-md-4{padding-right:1.5rem !important}.pb-md-4,.py-md-4{padding-bottom:1.5rem !important}.pl-md-4,.px-md-4{padding-left:1.5rem !important}.p-md-5{padding:3rem !important}.pt-md-5,.py-md-5{padding-top:3rem !important}.pr-md-5,.px-md-5{padding-right:3rem !important}.pb-md-5,.py-md-5{padding-bottom:3rem !important}.pl-md-5,.px-md-5{padding-left:3rem !important}.m-md-n1{margin:-.25rem !important}.mt-md-n1,.my-md-n1{margin-top:-.25rem !important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem !important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem !important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem !important}.m-md-n2{margin:-.5rem !important}.mt-md-n2,.my-md-n2{margin-top:-.5rem !important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem !important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem !important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem !important}.m-md-n3{margin:-1rem !important}.mt-md-n3,.my-md-n3{margin-top:-1rem !important}.mr-md-n3,.mx-md-n3{margin-right:-1rem !important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem !important}.ml-md-n3,.mx-md-n3{margin-left:-1rem !important}.m-md-n4{margin:-1.5rem !important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem !important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem !important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem !important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem !important}.m-md-n5{margin:-3rem !important}.mt-md-n5,.my-md-n5{margin-top:-3rem !important}.mr-md-n5,.mx-md-n5{margin-right:-3rem !important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem !important}.ml-md-n5,.mx-md-n5{margin-left:-3rem !important}.m-md-auto{margin:auto !important}.mt-md-auto,.my-md-auto{margin-top:auto !important}.mr-md-auto,.mx-md-auto{margin-right:auto !important}.mb-md-auto,.my-md-auto{margin-bottom:auto !important}.ml-md-auto,.mx-md-auto{margin-left:auto !important}}@media (min-width: 992px){.m-lg-0{margin:0 !important}.mt-lg-0,.my-lg-0{margin-top:0 !important}.mr-lg-0,.mx-lg-0{margin-right:0 !important}.mb-lg-0,.my-lg-0{margin-bottom:0 !important}.ml-lg-0,.mx-lg-0{margin-left:0 !important}.m-lg-1{margin:.25rem !important}.mt-lg-1,.my-lg-1{margin-top:.25rem !important}.mr-lg-1,.mx-lg-1{margin-right:.25rem !important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem !important}.ml-lg-1,.mx-lg-1{margin-left:.25rem !important}.m-lg-2{margin:.5rem !important}.mt-lg-2,.my-lg-2{margin-top:.5rem !important}.mr-lg-2,.mx-lg-2{margin-right:.5rem !important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem !important}.ml-lg-2,.mx-lg-2{margin-left:.5rem !important}.m-lg-3{margin:1rem !important}.mt-lg-3,.my-lg-3{margin-top:1rem !important}.mr-lg-3,.mx-lg-3{margin-right:1rem !important}.mb-lg-3,.my-lg-3{margin-bottom:1rem !important}.ml-lg-3,.mx-lg-3{margin-left:1rem !important}.m-lg-4{margin:1.5rem !important}.mt-lg-4,.my-lg-4{margin-top:1.5rem !important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem !important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem !important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem !important}.m-lg-5{margin:3rem !important}.mt-lg-5,.my-lg-5{margin-top:3rem !important}.mr-lg-5,.mx-lg-5{margin-right:3rem !important}.mb-lg-5,.my-lg-5{margin-bottom:3rem !important}.ml-lg-5,.mx-lg-5{margin-left:3rem !important}.p-lg-0{padding:0 !important}.pt-lg-0,.py-lg-0{padding-top:0 !important}.pr-lg-0,.px-lg-0{padding-right:0 !important}.pb-lg-0,.py-lg-0{padding-bottom:0 !important}.pl-lg-0,.px-lg-0{padding-left:0 !important}.p-lg-1{padding:.25rem !important}.pt-lg-1,.py-lg-1{padding-top:.25rem !important}.pr-lg-1,.px-lg-1{padding-right:.25rem !important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem !important}.pl-lg-1,.px-lg-1{padding-left:.25rem !important}.p-lg-2{padding:.5rem !important}.pt-lg-2,.py-lg-2{padding-top:.5rem !important}.pr-lg-2,.px-lg-2{padding-right:.5rem !important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem !important}.pl-lg-2,.px-lg-2{padding-left:.5rem !important}.p-lg-3{padding:1rem !important}.pt-lg-3,.py-lg-3{padding-top:1rem !important}.pr-lg-3,.px-lg-3{padding-right:1rem !important}.pb-lg-3,.py-lg-3{padding-bottom:1rem !important}.pl-lg-3,.px-lg-3{padding-left:1rem !important}.p-lg-4{padding:1.5rem !important}.pt-lg-4,.py-lg-4{padding-top:1.5rem !important}.pr-lg-4,.px-lg-4{padding-right:1.5rem !important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem !important}.pl-lg-4,.px-lg-4{padding-left:1.5rem !important}.p-lg-5{padding:3rem !important}.pt-lg-5,.py-lg-5{padding-top:3rem !important}.pr-lg-5,.px-lg-5{padding-right:3rem !important}.pb-lg-5,.py-lg-5{padding-bottom:3rem !important}.pl-lg-5,.px-lg-5{padding-left:3rem !important}.m-lg-n1{margin:-.25rem !important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem !important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem !important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem !important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem !important}.m-lg-n2{margin:-.5rem !important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem !important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem !important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem !important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem !important}.m-lg-n3{margin:-1rem !important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem !important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem !important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem !important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem !important}.m-lg-n4{margin:-1.5rem !important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem !important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem !important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem !important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem !important}.m-lg-n5{margin:-3rem !important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem !important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem !important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem !important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem !important}.m-lg-auto{margin:auto !important}.mt-lg-auto,.my-lg-auto{margin-top:auto !important}.mr-lg-auto,.mx-lg-auto{margin-right:auto !important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto !important}.ml-lg-auto,.mx-lg-auto{margin-left:auto !important}}@media (min-width: 1200px){.m-xl-0{margin:0 !important}.mt-xl-0,.my-xl-0{margin-top:0 !important}.mr-xl-0,.mx-xl-0{margin-right:0 !important}.mb-xl-0,.my-xl-0{margin-bottom:0 !important}.ml-xl-0,.mx-xl-0{margin-left:0 !important}.m-xl-1{margin:.25rem !important}.mt-xl-1,.my-xl-1{margin-top:.25rem !important}.mr-xl-1,.mx-xl-1{margin-right:.25rem !important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem !important}.ml-xl-1,.mx-xl-1{margin-left:.25rem !important}.m-xl-2{margin:.5rem !important}.mt-xl-2,.my-xl-2{margin-top:.5rem !important}.mr-xl-2,.mx-xl-2{margin-right:.5rem !important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem !important}.ml-xl-2,.mx-xl-2{margin-left:.5rem !important}.m-xl-3{margin:1rem !important}.mt-xl-3,.my-xl-3{margin-top:1rem !important}.mr-xl-3,.mx-xl-3{margin-right:1rem !important}.mb-xl-3,.my-xl-3{margin-bottom:1rem !important}.ml-xl-3,.mx-xl-3{margin-left:1rem !important}.m-xl-4{margin:1.5rem !important}.mt-xl-4,.my-xl-4{margin-top:1.5rem !important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem !important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem !important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem !important}.m-xl-5{margin:3rem !important}.mt-xl-5,.my-xl-5{margin-top:3rem !important}.mr-xl-5,.mx-xl-5{margin-right:3rem !important}.mb-xl-5,.my-xl-5{margin-bottom:3rem !important}.ml-xl-5,.mx-xl-5{margin-left:3rem !important}.p-xl-0{padding:0 !important}.pt-xl-0,.py-xl-0{padding-top:0 !important}.pr-xl-0,.px-xl-0{padding-right:0 !important}.pb-xl-0,.py-xl-0{padding-bottom:0 !important}.pl-xl-0,.px-xl-0{padding-left:0 !important}.p-xl-1{padding:.25rem !important}.pt-xl-1,.py-xl-1{padding-top:.25rem !important}.pr-xl-1,.px-xl-1{padding-right:.25rem !important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem !important}.pl-xl-1,.px-xl-1{padding-left:.25rem !important}.p-xl-2{padding:.5rem !important}.pt-xl-2,.py-xl-2{padding-top:.5rem !important}.pr-xl-2,.px-xl-2{padding-right:.5rem !important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem !important}.pl-xl-2,.px-xl-2{padding-left:.5rem !important}.p-xl-3{padding:1rem !important}.pt-xl-3,.py-xl-3{padding-top:1rem !important}.pr-xl-3,.px-xl-3{padding-right:1rem !important}.pb-xl-3,.py-xl-3{padding-bottom:1rem !important}.pl-xl-3,.px-xl-3{padding-left:1rem !important}.p-xl-4{padding:1.5rem !important}.pt-xl-4,.py-xl-4{padding-top:1.5rem !important}.pr-xl-4,.px-xl-4{padding-right:1.5rem !important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem !important}.pl-xl-4,.px-xl-4{padding-left:1.5rem !important}.p-xl-5{padding:3rem !important}.pt-xl-5,.py-xl-5{padding-top:3rem !important}.pr-xl-5,.px-xl-5{padding-right:3rem !important}.pb-xl-5,.py-xl-5{padding-bottom:3rem !important}.pl-xl-5,.px-xl-5{padding-left:3rem !important}.m-xl-n1{margin:-.25rem !important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem !important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem !important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem !important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem !important}.m-xl-n2{margin:-.5rem !important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem !important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem !important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem !important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem !important}.m-xl-n3{margin:-1rem !important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem !important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem !important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem !important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem !important}.m-xl-n4{margin:-1.5rem !important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem !important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem !important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem !important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem !important}.m-xl-n5{margin:-3rem !important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem !important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem !important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem !important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem !important}.m-xl-auto{margin:auto !important}.mt-xl-auto,.my-xl-auto{margin-top:auto !important}.mr-xl-auto,.mx-xl-auto{margin-right:auto !important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto !important}.ml-xl-auto,.mx-xl-auto{margin-left:auto !important}}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace !important}.text-justify{text-align:justify !important}.text-wrap{white-space:normal !important}.text-nowrap,.card-admin-table th,.card-admin-table .badges-list{white-space:nowrap !important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left !important}.text-right,.card-admin-table .badges-list{text-align:right !important}.text-center,.control-image-preview,.card-admin-stat,.card-admin-analytics-summary,.card-admin-table .row-select label,.card-admin-table .blankslate td{text-align:center !important}@media (min-width: 576px){.text-sm-left{text-align:left !important}.text-sm-right{text-align:right !important}.text-sm-center{text-align:center !important}}@media (min-width: 768px){.text-md-left{text-align:left !important}.text-md-right{text-align:right !important}.text-md-center{text-align:center !important}}@media (min-width: 992px){.text-lg-left{text-align:left !important}.text-lg-right{text-align:right !important}.text-lg-center{text-align:center !important}}@media (min-width: 1200px){.text-xl-left{text-align:left !important}.text-xl-right{text-align:right !important}.text-xl-center{text-align:center !important}}.text-lowercase{text-transform:lowercase !important}.text-uppercase{text-transform:uppercase !important}.text-capitalize{text-transform:capitalize !important}.font-weight-light{font-weight:300 !important}.font-weight-lighter{font-weight:lighter !important}.font-weight-normal{font-weight:400 !important}.font-weight-bold,.card-admin-table .item-name,.card-admin-table h5{font-weight:700 !important}.font-weight-bolder{font-weight:bolder !important}.font-italic{font-style:italic !important}.text-white{color:#fff !important}.text-primary{color:#6554c0 !important}a.text-primary:hover,a.text-primary:focus{color:#443692 !important}.text-secondary{color:#a5adba !important}a.text-secondary:hover,a.text-secondary:focus{color:#7a8699 !important}.text-success{color:#36b37e !important}a.text-success:hover,a.text-success:focus{color:#247855 !important}.text-info{color:#00b8d9 !important}a.text-info:hover,a.text-info:focus{color:#00778d !important}.text-warning{color:#ffc107 !important}a.text-warning:hover,a.text-warning:focus{color:#ba8b00 !important}.text-danger{color:#ff5630 !important}a.text-danger:hover,a.text-danger:focus{color:#e32a00 !important}.text-light{color:#ebecf0 !important}a.text-light:hover,a.text-light:focus{color:#bfc2cf !important}.text-dark{color:#505f79 !important}a.text-dark:hover,a.text-dark:focus{color:#323b4b !important}.text-body{color:#172b4d !important}.text-muted,.control-time-picker span,.control-image-preview div,.page-header h1 small{color:#a5adba !important}.text-black-50{color:rgba(9,30,66,0.5) !important}.text-white-50{color:rgba(255,255,255,0.5) !important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none !important}.text-break{word-break:break-word !important;overflow-wrap:break-word !important}.text-reset{color:inherit !important}.visible{visibility:visible !important}.invisible{visibility:hidden !important}html,body{height:100%}.control-checkboxselect .form-check,.control-radioselect .form-check{padding:0}.control-checkboxselect .form-check-label,.control-radioselect .form-check-label{font-size:.875rem}.control-month-picker{width:350px}.control-time-picker{width:280px;height:100%;font-size:4rem}.control-time-picker .row{height:100%}.control-time-picker input{font-size:4rem}.control-time-picker span{position:relative;bottom:.25rem}.control-image-preview{background-color:#fff}.control-image-preview img{max-width:300px;max-height:200px}.control-image-preview div{font-size:.875rem}.login-form{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.login-form-container{width:100%;padding:30px}.login-form-logo{margin-bottom:2rem;text-align:center}.login-form-logo img{max-width:200px;max-height:64px}.login-form-card{max-width:340px;margin:0 auto}.login-form-title{font-size:1.25rem;text-align:center}.navbar-brand{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.navbar-brand img{height:2rem;margin-right:.5rem;border-radius:.25rem}.navbar .btn-user{overflow:hidden}.navbar .btn-user img{height:2rem}.col-nav-side{max-width:240px}.nav-side .media-icon{width:30px;text-align:center}.nav-side .nav-link.active{color:#091e42}.nav-side .nav-link.active .media-icon{color:#6554c0}.nav-side .nav-section.active{font-weight:700}.nav-side .nav-action .media-body{margin-left:30px}.page-header h1{font-size:1.25rem}.page-header h1 a{color:#172b4d}.page-header h1 small{font-size:1.25rem;vertical-align:middle}.card-admin-settings-card{color:#172b4d}.card-admin-settings-card:hover,.card-admin-settings-card:focus{text-decoration:none;background-color:#ebecf0}.card-admin-settings-card h5{font-size:1rem}.card-admin-info .card-title{font-size:1.25rem}.card-admin-info .card-body,.card-admin-info .card-admin-form .form-fieldset,.card-admin-form .card-admin-info .form-fieldset{padding:.75rem}.card-admin-stat{font-size:1.25rem}.media-admin-check{font-size:.875rem}.media-admin-check h5{font-size:1rem}.media-check-icon{width:1.5rem;height:1.5rem;font-size:1rem;line-height:1rem;color:#fff;background:#6b778c}.media-check-icon.media-check-icon-warning{background:#ffab00}.media-check-icon.media-check-icon-danger{background:#ff5630}.media-check-icon.media-check-icon-success{background:#36b37e}.media-check-icon .spinner-border{width:1rem;height:1rem}.card-admin-analytics-summary{width:200px}.card-admin-analytics-summary div{font-size:2rem}.card-admin-analytics-summary small{font-size:1rem}.card-admin-analytics-chart{margin-top:-10px}.card-admin-table .card-title{font-size:1.25rem}.card-admin-table th,.card-admin-table td{vertical-align:middle}.card-admin-table th{font-size:.875rem;color:#6b778c;background-color:rgba(0,0,0,0)}.card-admin-table .row-select input,.card-admin-table th input{font-size:1.25rem}.card-admin-table .card-body,.card-admin-table .card-admin-form .form-fieldset,.card-admin-form .card-admin-table .form-fieldset{padding:.75rem}.card-admin-table table+.card-body,.card-admin-table .card-admin-form table+.form-fieldset,.card-admin-form .card-admin-table table+.form-fieldset{border-top:1px solid #dfe1e6}.card-admin-table .btn-thumbnail{width:2rem;height:2rem;padding:0;background-color:transparent;background-size:cover}.card-admin-table .item-name{color:#172b4d}.card-admin-table h5{font-size:.875rem;line-height:1.5}.card-admin-table h5 a{color:#172b4d}.card-admin-table .badges-list{width:0%}.card-admin-table .row-select{width:1px}.card-admin-table [data-timestamp]{text-decoration:none}.card-admin-form .card-header h5{font-size:1.25rem}.card-admin-form .form-fieldset{margin:0 -1.25rem}.login-error{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.login-error-container{width:100%;padding:30px}.login-error-card{max-width:540px;margin:0 auto}.login-error-title{font-size:1.25rem;text-align:center}
+:root{--blue: #0052cc;--indigo: #6610f2;--purple: #6554c0;--pink: #e83e8c;--red: #ff5630;--orange: #ffab00;--yellow: #ffc107;--green: #36b37e;--teal: #20c997;--cyan: #00b8d9;--white: #fff;--gray: #a5adba;--gray-dark: #505f79;--primary: #6554c0;--secondary: #a5adba;--success: #36b37e;--info: #00b8d9;--warning: #ffc107;--danger: #ff5630;--light: #ebecf0;--dark: #505f79;--breakpoint-xs: 0;--breakpoint-sm: 576px;--breakpoint-md: 768px;--breakpoint-lg: 992px;--breakpoint-xl: 1200px;--font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}*,*::before,*::after{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(9,30,66,0)}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#172b4d;text-align:left;background-color:#f4f5f7}[tabindex="-1"]:focus{outline:0 !important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[title],abbr[data-original-title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul,dl{margin-top:0;margin-bottom:1rem}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#6b778c;text-decoration:none;background-color:transparent}a:hover{color:#172b4d;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):hover,a:not([href]):not([tabindex]):focus{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}pre,code,kbd,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#a5adba;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}input,button,select,optgroup,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}button,[type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button:not(:disabled),[type="button"]:not(:disabled),[type="reset"]:not(:disabled),[type="submit"]:not(:disabled){cursor:pointer}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{padding:0;border-style:none}input[type="radio"],input[type="checkbox"]{box-sizing:border-box;padding:0}input[type="date"],input[type="time"],input[type="datetime-local"],input[type="month"]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{outline-offset:-2px;-webkit-appearance:none}[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none !important}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{margin-bottom:.5rem;font-weight:500;line-height:1.2}h1,.h1{font-size:2.5rem}h2,.h2{font-size:2rem}h3,.h3{font-size:1.75rem}h4,.h4{font-size:1.5rem}h5,.h5{font-size:1.25rem}h6,.h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(9,30,66,0.1)}small,.small{font-size:80%;font-weight:400}mark,.mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#a5adba}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#f4f5f7;border:1px solid #dfe1e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#a5adba}code{font-size:87.5%;color:#e83e8c;word-break:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#172b4d;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#172b4d}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width: 576px){.container{max-width:540px}}@media (min-width: 768px){.container{max-width:720px}}@media (min-width: 992px){.container{max-width:960px}}@media (min-width: 1200px){.container{max-width:1140px}}.container-fluid{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*="col-"]{padding-right:0;padding-left:0}.col-1,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-10,.col-11,.col-12,.col,.col-auto,.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm,.col-sm-auto,.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12,.col-md,.col-md-auto,.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg,.col-lg-auto,.col-xl-1,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-ms-flex:0 0 8.33333%;flex:0 0 8.33333%;max-width:8.33333%}.col-2{-ms-flex:0 0 16.66667%;flex:0 0 16.66667%;max-width:16.66667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.33333%;flex:0 0 33.33333%;max-width:33.33333%}.col-5{-ms-flex:0 0 41.66667%;flex:0 0 41.66667%;max-width:41.66667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.33333%;flex:0 0 58.33333%;max-width:58.33333%}.col-8{-ms-flex:0 0 66.66667%;flex:0 0 66.66667%;max-width:66.66667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.33333%;flex:0 0 83.33333%;max-width:83.33333%}.col-11{-ms-flex:0 0 91.66667%;flex:0 0 91.66667%;max-width:91.66667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.33333%}.offset-2{margin-left:16.66667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333%}.offset-5{margin-left:41.66667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333%}.offset-8{margin-left:66.66667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333%}.offset-11{margin-left:91.66667%}@media (min-width: 576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-ms-flex:0 0 8.33333%;flex:0 0 8.33333%;max-width:8.33333%}.col-sm-2{-ms-flex:0 0 16.66667%;flex:0 0 16.66667%;max-width:16.66667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.33333%;flex:0 0 33.33333%;max-width:33.33333%}.col-sm-5{-ms-flex:0 0 41.66667%;flex:0 0 41.66667%;max-width:41.66667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.33333%;flex:0 0 58.33333%;max-width:58.33333%}.col-sm-8{-ms-flex:0 0 66.66667%;flex:0 0 66.66667%;max-width:66.66667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.33333%;flex:0 0 83.33333%;max-width:83.33333%}.col-sm-11{-ms-flex:0 0 91.66667%;flex:0 0 91.66667%;max-width:91.66667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333%}.offset-sm-2{margin-left:16.66667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333%}.offset-sm-5{margin-left:41.66667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333%}.offset-sm-8{margin-left:66.66667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333%}.offset-sm-11{margin-left:91.66667%}}@media (min-width: 768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-ms-flex:0 0 8.33333%;flex:0 0 8.33333%;max-width:8.33333%}.col-md-2{-ms-flex:0 0 16.66667%;flex:0 0 16.66667%;max-width:16.66667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.33333%;flex:0 0 33.33333%;max-width:33.33333%}.col-md-5{-ms-flex:0 0 41.66667%;flex:0 0 41.66667%;max-width:41.66667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.33333%;flex:0 0 58.33333%;max-width:58.33333%}.col-md-8{-ms-flex:0 0 66.66667%;flex:0 0 66.66667%;max-width:66.66667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.33333%;flex:0 0 83.33333%;max-width:83.33333%}.col-md-11{-ms-flex:0 0 91.66667%;flex:0 0 91.66667%;max-width:91.66667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333%}.offset-md-2{margin-left:16.66667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333%}.offset-md-5{margin-left:41.66667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333%}.offset-md-8{margin-left:66.66667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333%}.offset-md-11{margin-left:91.66667%}}@media (min-width: 992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-ms-flex:0 0 8.33333%;flex:0 0 8.33333%;max-width:8.33333%}.col-lg-2{-ms-flex:0 0 16.66667%;flex:0 0 16.66667%;max-width:16.66667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.33333%;flex:0 0 33.33333%;max-width:33.33333%}.col-lg-5{-ms-flex:0 0 41.66667%;flex:0 0 41.66667%;max-width:41.66667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.33333%;flex:0 0 58.33333%;max-width:58.33333%}.col-lg-8{-ms-flex:0 0 66.66667%;flex:0 0 66.66667%;max-width:66.66667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.33333%;flex:0 0 83.33333%;max-width:83.33333%}.col-lg-11{-ms-flex:0 0 91.66667%;flex:0 0 91.66667%;max-width:91.66667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333%}.offset-lg-2{margin-left:16.66667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333%}.offset-lg-5{margin-left:41.66667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333%}.offset-lg-8{margin-left:66.66667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333%}.offset-lg-11{margin-left:91.66667%}}@media (min-width: 1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-ms-flex:0 0 8.33333%;flex:0 0 8.33333%;max-width:8.33333%}.col-xl-2{-ms-flex:0 0 16.66667%;flex:0 0 16.66667%;max-width:16.66667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.33333%;flex:0 0 33.33333%;max-width:33.33333%}.col-xl-5{-ms-flex:0 0 41.66667%;flex:0 0 41.66667%;max-width:41.66667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.33333%;flex:0 0 58.33333%;max-width:58.33333%}.col-xl-8{-ms-flex:0 0 66.66667%;flex:0 0 66.66667%;max-width:66.66667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.33333%;flex:0 0 83.33333%;max-width:83.33333%}.col-xl-11{-ms-flex:0 0 91.66667%;flex:0 0 91.66667%;max-width:91.66667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333%}.offset-xl-2{margin-left:16.66667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333%}.offset-xl-5{margin-left:41.66667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333%}.offset-xl-8{margin-left:66.66667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333%}.offset-xl-11{margin-left:91.66667%}}.table{width:100%;margin-bottom:1rem;color:#172b4d}.table th,.table td{padding:.75rem;vertical-align:top;border-top:1px solid #dfe1e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dfe1e6}.table tbody+tbody{border-top:2px solid #dfe1e6}.table-sm th,.table-sm td{padding:.3rem}.table-bordered{border:1px solid #dfe1e6}.table-bordered th,.table-bordered td{border:1px solid #dfe1e6}.table-bordered thead th,.table-bordered thead td{border-bottom-width:2px}.table-borderless th,.table-borderless td,.table-borderless thead th,.table-borderless tbody+tbody{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(9,30,66,0.05)}.table-hover tbody tr:hover{color:#172b4d;background-color:rgba(9,30,66,0.075)}.table-primary,.table-primary>th,.table-primary>td{background-color:#d4cfed}.table-primary th,.table-primary td,.table-primary thead th,.table-primary tbody+tbody{border-color:#afa6de}.table-hover .table-primary:hover{background-color:#c3bce6}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#c3bce6}.table-secondary,.table-secondary>th,.table-secondary>td{background-color:#e6e8ec}.table-secondary th,.table-secondary td,.table-secondary thead th,.table-secondary tbody+tbody{border-color:#d0d4db}.table-hover .table-secondary:hover{background-color:#d8dbe1}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#d8dbe1}.table-success,.table-success>th,.table-success>td{background-color:#c7eadb}.table-success th,.table-success td,.table-success thead th,.table-success tbody+tbody{border-color:#96d7bc}.table-hover .table-success:hover{background-color:#b4e3cf}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b4e3cf}.table-info,.table-info>th,.table-info>td{background-color:#b8ebf4}.table-info th,.table-info td,.table-info thead th,.table-info tbody+tbody{border-color:#7adaeb}.table-hover .table-info:hover{background-color:#a2e5f1}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#a2e5f1}.table-warning,.table-warning>th,.table-warning>td{background-color:#ffeeba}.table-warning th,.table-warning td,.table-warning thead th,.table-warning tbody+tbody{border-color:#ffdf7e}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>th,.table-danger>td{background-color:#ffd0c5}.table-danger th,.table-danger td,.table-danger thead th,.table-danger tbody+tbody{border-color:#ffa793}.table-hover .table-danger:hover{background-color:#ffbbac}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#ffbbac}.table-light,.table-light>th,.table-light>td{background-color:#f9fafb}.table-light th,.table-light td,.table-light thead th,.table-light tbody+tbody{border-color:#f5f5f7}.table-hover .table-light:hover{background-color:#eaedf1}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#eaedf1}.table-dark,.table-dark>th,.table-dark>td{background-color:#ced2d9}.table-dark th,.table-dark td,.table-dark thead th,.table-dark tbody+tbody{border-color:#a4acb9}.table-hover .table-dark:hover{background-color:#c0c5ce}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#c0c5ce}.table-active,.table-active>th,.table-active>td{background-color:rgba(9,30,66,0.075)}.table-hover .table-active:hover{background-color:rgba(6,20,44,0.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(6,20,44,0.075)}.table .thead-dark th{color:#fff;background-color:#505f79;border-color:#5f7190}.table .thead-light th{color:#6b778c;background-color:rgba(0,0,0,0);border-color:#dfe1e6}.table-dark{color:#fff;background-color:#505f79}.table-dark th,.table-dark td,.table-dark thead th{border-color:#5f7190}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,0.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,0.075)}@media (max-width: 575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width: 767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width: 991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width: 1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#6b778c;background-color:#fff;background-clip:padding-box;border:1px solid #c1c7d0;border-radius:.25rem;transition:border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#6b778c;background-color:#fff;border-color:#1851b2;outline:0;box-shadow:0 0 0 .2rem rgba(0,82,204,0.15)}.form-control::-webkit-input-placeholder{color:#a5adba;opacity:1}.form-control::-moz-placeholder{color:#a5adba;opacity:1}.form-control:-ms-input-placeholder{color:#a5adba;opacity:1}.form-control::-ms-input-placeholder{color:#a5adba;opacity:1}.form-control::placeholder{color:#a5adba;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#ebecf0;opacity:1}select.form-control:focus::-ms-value{color:#6b778c;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding-top:.375rem;padding-bottom:.375rem;margin-bottom:0;line-height:1.5;color:#172b4d;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-sm,.form-control-plaintext.form-control-lg{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[size],select.form-control[multiple]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*="col-"]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled ~ .form-check-label{color:#a5adba}.form-check-label{margin-bottom:0}.form-check-inline{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#36b37e}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(54,179,126,0.9);border-radius:.25rem}.was-validated .form-control:valid,.form-control.is-valid{border-color:#36b37e;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2336b37e' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:center right calc(.375em + .1875rem);background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.was-validated .form-control:valid:focus,.form-control.is-valid:focus{border-color:#36b37e;box-shadow:0 0 0 .2rem rgba(54,179,126,0.25)}.was-validated .form-control:valid ~ .valid-feedback,.was-validated .form-control:valid ~ .valid-tooltip,.form-control.is-valid ~ .valid-feedback,.form-control.is-valid ~ .valid-tooltip{display:block}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.was-validated .custom-select:valid,.custom-select.is-valid{border-color:#36b37e;padding-right:calc((1em + .75rem) * 3 / 4 + 1.75rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23505f79' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2336b37e' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.was-validated .custom-select:valid:focus,.custom-select.is-valid:focus{border-color:#36b37e;box-shadow:0 0 0 .2rem rgba(54,179,126,0.25)}.was-validated .custom-select:valid ~ .valid-feedback,.was-validated .custom-select:valid ~ .valid-tooltip,.custom-select.is-valid ~ .valid-feedback,.custom-select.is-valid ~ .valid-tooltip{display:block}.was-validated .form-control-file:valid ~ .valid-feedback,.was-validated .form-control-file:valid ~ .valid-tooltip,.form-control-file.is-valid ~ .valid-feedback,.form-control-file.is-valid ~ .valid-tooltip{display:block}.was-validated .form-check-input:valid ~ .form-check-label,.form-check-input.is-valid ~ .form-check-label{color:#36b37e}.was-validated .form-check-input:valid ~ .valid-feedback,.was-validated .form-check-input:valid ~ .valid-tooltip,.form-check-input.is-valid ~ .valid-feedback,.form-check-input.is-valid ~ .valid-tooltip{display:block}.was-validated .custom-control-input:valid ~ .custom-control-label,.custom-control-input.is-valid ~ .custom-control-label{color:#36b37e}.was-validated .custom-control-input:valid ~ .custom-control-label::before,.custom-control-input.is-valid ~ .custom-control-label::before{border-color:#36b37e}.was-validated .custom-control-input:valid ~ .valid-feedback,.was-validated .custom-control-input:valid ~ .valid-tooltip,.custom-control-input.is-valid ~ .valid-feedback,.custom-control-input.is-valid ~ .valid-tooltip{display:block}.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before,.custom-control-input.is-valid:checked ~ .custom-control-label::before{border-color:#51cb97;background-color:#51cb97}.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before,.custom-control-input.is-valid:focus ~ .custom-control-label::before{box-shadow:0 0 0 .2rem rgba(54,179,126,0.25)}.was-validated .custom-control-input:valid:focus:not(:checked) ~ .custom-control-label::before,.custom-control-input.is-valid:focus:not(:checked) ~ .custom-control-label::before{border-color:#36b37e}.was-validated .custom-file-input:valid ~ .custom-file-label,.custom-file-input.is-valid ~ .custom-file-label{border-color:#36b37e}.was-validated .custom-file-input:valid ~ .valid-feedback,.was-validated .custom-file-input:valid ~ .valid-tooltip,.custom-file-input.is-valid ~ .valid-feedback,.custom-file-input.is-valid ~ .valid-tooltip{display:block}.was-validated .custom-file-input:valid:focus ~ .custom-file-label,.custom-file-input.is-valid:focus ~ .custom-file-label{border-color:#36b37e;box-shadow:0 0 0 .2rem rgba(54,179,126,0.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#ff5630}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(255,86,48,0.9);border-radius:.25rem}.was-validated .form-control:invalid,.form-control.is-invalid{border-color:#ff5630;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23ff5630' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23ff5630' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E");background-repeat:no-repeat;background-position:center right calc(.375em + .1875rem);background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.was-validated .form-control:invalid:focus,.form-control.is-invalid:focus{border-color:#ff5630;box-shadow:0 0 0 .2rem rgba(255,86,48,0.25)}.was-validated .form-control:invalid ~ .invalid-feedback,.was-validated .form-control:invalid ~ .invalid-tooltip,.form-control.is-invalid ~ .invalid-feedback,.form-control.is-invalid ~ .invalid-tooltip{display:block}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.was-validated .custom-select:invalid,.custom-select.is-invalid{border-color:#ff5630;padding-right:calc((1em + .75rem) * 3 / 4 + 1.75rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23505f79' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23ff5630' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23ff5630' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.was-validated .custom-select:invalid:focus,.custom-select.is-invalid:focus{border-color:#ff5630;box-shadow:0 0 0 .2rem rgba(255,86,48,0.25)}.was-validated .custom-select:invalid ~ .invalid-feedback,.was-validated .custom-select:invalid ~ .invalid-tooltip,.custom-select.is-invalid ~ .invalid-feedback,.custom-select.is-invalid ~ .invalid-tooltip{display:block}.was-validated .form-control-file:invalid ~ .invalid-feedback,.was-validated .form-control-file:invalid ~ .invalid-tooltip,.form-control-file.is-invalid ~ .invalid-feedback,.form-control-file.is-invalid ~ .invalid-tooltip{display:block}.was-validated .form-check-input:invalid ~ .form-check-label,.form-check-input.is-invalid ~ .form-check-label{color:#ff5630}.was-validated .form-check-input:invalid ~ .invalid-feedback,.was-validated .form-check-input:invalid ~ .invalid-tooltip,.form-check-input.is-invalid ~ .invalid-feedback,.form-check-input.is-invalid ~ .invalid-tooltip{display:block}.was-validated .custom-control-input:invalid ~ .custom-control-label,.custom-control-input.is-invalid ~ .custom-control-label{color:#ff5630}.was-validated .custom-control-input:invalid ~ .custom-control-label::before,.custom-control-input.is-invalid ~ .custom-control-label::before{border-color:#ff5630}.was-validated .custom-control-input:invalid ~ .invalid-feedback,.was-validated .custom-control-input:invalid ~ .invalid-tooltip,.custom-control-input.is-invalid ~ .invalid-feedback,.custom-control-input.is-invalid ~ .invalid-tooltip{display:block}.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before,.custom-control-input.is-invalid:checked ~ .custom-control-label::before{border-color:#ff8063;background-color:#ff8063}.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before,.custom-control-input.is-invalid:focus ~ .custom-control-label::before{box-shadow:0 0 0 .2rem rgba(255,86,48,0.25)}.was-validated .custom-control-input:invalid:focus:not(:checked) ~ .custom-control-label::before,.custom-control-input.is-invalid:focus:not(:checked) ~ .custom-control-label::before{border-color:#ff5630}.was-validated .custom-file-input:invalid ~ .custom-file-label,.custom-file-input.is-invalid ~ .custom-file-label{border-color:#ff5630}.was-validated .custom-file-input:invalid ~ .invalid-feedback,.was-validated .custom-file-input:invalid ~ .invalid-tooltip,.custom-file-input.is-invalid ~ .invalid-feedback,.custom-file-input.is-invalid ~ .invalid-tooltip{display:block}.was-validated .custom-file-input:invalid:focus ~ .custom-file-label,.custom-file-input.is-invalid:focus ~ .custom-file-label{border-color:#ff5630;box-shadow:0 0 0 .2rem rgba(255,86,48,0.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width: 576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .input-group,.form-inline .custom-select{width:auto}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;-ms-flex-negative:0;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#172b4d;text-align:center;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.btn{transition:none}}.btn:hover{color:#172b4d;text-decoration:none}.btn:focus,.btn.focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,82,204,0.15)}.btn.disabled,.btn:disabled{opacity:.65}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#6554c0;border-color:#6554c0}.btn-primary:hover{color:#fff;background-color:#5140ae;border-color:#4d3da4}.btn-primary:focus,.btn-primary.focus{box-shadow:0 0 0 .2rem rgba(124,110,201,0.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#6554c0;border-color:#6554c0}.btn-primary:not(:disabled):not(.disabled):active,.btn-primary:not(:disabled):not(.disabled).active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#4d3da4;border-color:#49399b}.btn-primary:not(:disabled):not(.disabled):active:focus,.btn-primary:not(:disabled):not(.disabled).active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(124,110,201,0.5)}.btn-secondary{color:#172b4d;background-color:#a5adba;border-color:#a5adba}.btn-secondary:hover{color:#172b4d;background-color:#8f99a9;border-color:#8893a4}.btn-secondary:focus,.btn-secondary.focus{box-shadow:0 0 0 .2rem rgba(144,154,170,0.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#172b4d;background-color:#a5adba;border-color:#a5adba}.btn-secondary:not(:disabled):not(.disabled):active,.btn-secondary:not(:disabled):not(.disabled).active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#8893a4;border-color:#818c9e}.btn-secondary:not(:disabled):not(.disabled):active:focus,.btn-secondary:not(:disabled):not(.disabled).active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(144,154,170,0.5)}.btn-success{color:#fff;background-color:#36b37e;border-color:#36b37e}.btn-success:hover{color:#fff;background-color:#2d9669;border-color:#2a8c62}.btn-success:focus,.btn-success.focus{box-shadow:0 0 0 .2rem rgba(84,190,145,0.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#36b37e;border-color:#36b37e}.btn-success:not(:disabled):not(.disabled):active,.btn-success:not(:disabled):not(.disabled).active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#2a8c62;border-color:#27825c}.btn-success:not(:disabled):not(.disabled):active:focus,.btn-success:not(:disabled):not(.disabled).active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(84,190,145,0.5)}.btn-info{color:#fff;background-color:#00b8d9;border-color:#00b8d9}.btn-info:hover{color:#fff;background-color:#0098b3;border-color:#008da6}.btn-info:focus,.btn-info.focus{box-shadow:0 0 0 .2rem rgba(38,195,223,0.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#00b8d9;border-color:#00b8d9}.btn-info:not(:disabled):not(.disabled):active,.btn-info:not(:disabled):not(.disabled).active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#008da6;border-color:#008299}.btn-info:not(:disabled):not(.disabled):active:focus,.btn-info:not(:disabled):not(.disabled).active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(38,195,223,0.5)}.btn-warning{color:#172b4d;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#172b4d;background-color:#e0a800;border-color:#d39e00}.btn-warning:focus,.btn-warning.focus{box-shadow:0 0 0 .2rem rgba(220,171,18,0.5)}.btn-warning.disabled,.btn-warning:disabled{color:#172b4d;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled):active,.btn-warning:not(:disabled):not(.disabled).active,.show>.btn-warning.dropdown-toggle{color:#172b4d;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled):active:focus,.btn-warning:not(:disabled):not(.disabled).active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,171,18,0.5)}.btn-danger{color:#fff;background-color:#ff5630;border-color:#ff5630}.btn-danger:hover{color:#fff;background-color:#ff370a;border-color:#fc2e00}.btn-danger:focus,.btn-danger.focus{box-shadow:0 0 0 .2rem rgba(255,111,79,0.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#ff5630;border-color:#ff5630}.btn-danger:not(:disabled):not(.disabled):active,.btn-danger:not(:disabled):not(.disabled).active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#fc2e00;border-color:#ef2c00}.btn-danger:not(:disabled):not(.disabled):active:focus,.btn-danger:not(:disabled):not(.disabled).active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,111,79,0.5)}.btn-light,.navbar .btn-user,.card-admin-table .btn-thumbnail{color:#172b4d;background-color:#ebecf0;border-color:#ebecf0}.btn-light:hover,.navbar .btn-user:hover,.card-admin-table .btn-thumbnail:hover{color:#172b4d;background-color:#d5d7e0;border-color:#ced0da}.btn-light:focus,.navbar .btn-user:focus,.card-admin-table .btn-thumbnail:focus,.btn-light.focus,.navbar .focus.btn-user,.card-admin-table .focus.btn-thumbnail{box-shadow:0 0 0 .2rem rgba(203,207,216,0.5)}.btn-light.disabled,.navbar .disabled.btn-user,.card-admin-table .disabled.btn-thumbnail,.btn-light:disabled,.navbar .btn-user:disabled,.card-admin-table .btn-thumbnail:disabled{color:#172b4d;background-color:#ebecf0;border-color:#ebecf0}.btn-light:not(:disabled):not(.disabled):active,.navbar .btn-user:not(:disabled):not(.disabled):active,.card-admin-table .btn-thumbnail:not(:disabled):not(.disabled):active,.btn-light:not(:disabled):not(.disabled).active,.navbar .btn-user:not(:disabled):not(.disabled).active,.card-admin-table .btn-thumbnail:not(:disabled):not(.disabled).active,.show>.btn-light.dropdown-toggle,.navbar .show>.dropdown-toggle.btn-user,.card-admin-table .show>.dropdown-toggle.btn-thumbnail{color:#172b4d;background-color:#ced0da;border-color:#c7c9d5}.btn-light:not(:disabled):not(.disabled):active:focus,.navbar .btn-user:not(:disabled):not(.disabled):active:focus,.card-admin-table .btn-thumbnail:not(:disabled):not(.disabled):active:focus,.btn-light:not(:disabled):not(.disabled).active:focus,.navbar .btn-user:not(:disabled):not(.disabled).active:focus,.card-admin-table .btn-thumbnail:not(:disabled):not(.disabled).active:focus,.show>.btn-light.dropdown-toggle:focus,.navbar .show>.dropdown-toggle.btn-user:focus,.card-admin-table .show>.dropdown-toggle.btn-thumbnail:focus{box-shadow:0 0 0 .2rem rgba(203,207,216,0.5)}.btn-dark{color:#fff;background-color:#505f79;border-color:#505f79}.btn-dark:hover{color:#fff;background-color:#414d62;border-color:#3c475a}.btn-dark:focus,.btn-dark.focus{box-shadow:0 0 0 .2rem rgba(106,119,141,0.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#505f79;border-color:#505f79}.btn-dark:not(:disabled):not(.disabled):active,.btn-dark:not(:disabled):not(.disabled).active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#3c475a;border-color:#374153}.btn-dark:not(:disabled):not(.disabled):active:focus,.btn-dark:not(:disabled):not(.disabled).active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(106,119,141,0.5)}.btn-outline-primary{color:#6554c0;border-color:#6554c0}.btn-outline-primary:hover{color:#fff;background-color:#6554c0;border-color:#6554c0}.btn-outline-primary:focus,.btn-outline-primary.focus{box-shadow:0 0 0 .2rem rgba(101,84,192,0.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#6554c0;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled):active,.btn-outline-primary:not(:disabled):not(.disabled).active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#6554c0;border-color:#6554c0}.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(101,84,192,0.5)}.btn-outline-secondary{color:#a5adba;border-color:#a5adba}.btn-outline-secondary:hover{color:#172b4d;background-color:#a5adba;border-color:#a5adba}.btn-outline-secondary:focus,.btn-outline-secondary.focus{box-shadow:0 0 0 .2rem rgba(165,173,186,0.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#a5adba;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled):active,.btn-outline-secondary:not(:disabled):not(.disabled).active,.show>.btn-outline-secondary.dropdown-toggle{color:#172b4d;background-color:#a5adba;border-color:#a5adba}.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(165,173,186,0.5)}.btn-outline-success{color:#36b37e;border-color:#36b37e}.btn-outline-success:hover{color:#fff;background-color:#36b37e;border-color:#36b37e}.btn-outline-success:focus,.btn-outline-success.focus{box-shadow:0 0 0 .2rem rgba(54,179,126,0.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#36b37e;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled):active,.btn-outline-success:not(:disabled):not(.disabled).active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#36b37e;border-color:#36b37e}.btn-outline-success:not(:disabled):not(.disabled):active:focus,.btn-outline-success:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(54,179,126,0.5)}.btn-outline-info{color:#00b8d9;border-color:#00b8d9}.btn-outline-info:hover{color:#fff;background-color:#00b8d9;border-color:#00b8d9}.btn-outline-info:focus,.btn-outline-info.focus{box-shadow:0 0 0 .2rem rgba(0,184,217,0.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#00b8d9;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled):active,.btn-outline-info:not(:disabled):not(.disabled).active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#00b8d9;border-color:#00b8d9}.btn-outline-info:not(:disabled):not(.disabled):active:focus,.btn-outline-info:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,184,217,0.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#172b4d;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:focus,.btn-outline-warning.focus{box-shadow:0 0 0 .2rem rgba(255,193,7,0.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled):active,.btn-outline-warning:not(:disabled):not(.disabled).active,.show>.btn-outline-warning.dropdown-toggle{color:#172b4d;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,0.5)}.btn-outline-danger{color:#ff5630;border-color:#ff5630}.btn-outline-danger:hover{color:#fff;background-color:#ff5630;border-color:#ff5630}.btn-outline-danger:focus,.btn-outline-danger.focus{box-shadow:0 0 0 .2rem rgba(255,86,48,0.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#ff5630;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled):active,.btn-outline-danger:not(:disabled):not(.disabled).active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#ff5630;border-color:#ff5630}.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,86,48,0.5)}.btn-outline-light{color:#ebecf0;border-color:#ebecf0}.btn-outline-light:hover{color:#172b4d;background-color:#ebecf0;border-color:#ebecf0}.btn-outline-light:focus,.btn-outline-light.focus{box-shadow:0 0 0 .2rem rgba(235,236,240,0.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#ebecf0;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled):active,.btn-outline-light:not(:disabled):not(.disabled).active,.show>.btn-outline-light.dropdown-toggle{color:#172b4d;background-color:#ebecf0;border-color:#ebecf0}.btn-outline-light:not(:disabled):not(.disabled):active:focus,.btn-outline-light:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(235,236,240,0.5)}.btn-outline-dark{color:#505f79;border-color:#505f79}.btn-outline-dark:hover{color:#fff;background-color:#505f79;border-color:#505f79}.btn-outline-dark:focus,.btn-outline-dark.focus{box-shadow:0 0 0 .2rem rgba(80,95,121,0.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#505f79;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled):active,.btn-outline-dark:not(:disabled):not(.disabled).active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#505f79;border-color:#505f79}.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(80,95,121,0.5)}.btn-link{font-weight:400;color:#6b778c;text-decoration:none}.btn-link:hover{color:#172b4d;text-decoration:underline}.btn-link:focus,.btn-link.focus{text-decoration:underline;box-shadow:none}.btn-link:disabled,.btn-link.disabled{color:#a5adba;pointer-events:none}.btn-lg,.btn-group-lg>.btn{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-sm,.btn-group-sm>.btn{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block,.card-admin-table .btn-thumbnail{display:block;width:100%}.btn-block+.btn-block,.card-admin-table .btn-thumbnail+.btn-block,.card-admin-table .btn-block+.btn-thumbnail,.card-admin-table .btn-thumbnail+.btn-thumbnail{margin-top:.5rem}input[type="submit"].btn-block,.card-admin-table input.btn-thumbnail[type="submit"],input[type="reset"].btn-block,.card-admin-table input.btn-thumbnail[type="reset"],input[type="button"].btn-block,.card-admin-table input.btn-thumbnail[type="button"]{width:100%}.fade{transition:opacity 0.15s linear}@media (prefers-reduced-motion: reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height 0.35s ease}@media (prefers-reduced-motion: reduce){.collapsing{transition:none}}.dropup,.dropright,.dropdown,.dropleft{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#172b4d;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(9,30,66,0.15);border-radius:.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width: 576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width: 768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width: 992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width: 1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^="top"],.dropdown-menu[x-placement^="right"],.dropdown-menu[x-placement^="bottom"],.dropdown-menu[x-placement^="left"]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #ebecf0}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#172b4d;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:hover,.dropdown-item:focus{color:#112039;text-decoration:none;background-color:#f4f5f7}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#091e42}.dropdown-item.disabled,.dropdown-item:disabled{color:#a5adba;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#a5adba;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#172b4d}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover{z-index:1}.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn.active{z-index:1}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:not(:first-child),.btn-group>.btn-group:not(:first-child){margin-left:-1px}.btn-group>.btn:not(:last-child):not(.dropdown-toggle),.btn-group>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:not(:first-child),.btn-group>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-sm+.dropdown-toggle-split,.btn-group-sm>.btn+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-lg+.dropdown-toggle-split,.btn-group-lg>.btn+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle),.btn-group-vertical>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type="radio"],.btn-group-toggle>.btn input[type="checkbox"],.btn-group-toggle>.btn-group>.btn input[type="radio"],.btn-group-toggle>.btn-group>.btn input[type="checkbox"]{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-control-plaintext,.input-group>.custom-select,.input-group>.custom-file{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;margin-bottom:0}.input-group>.form-control+.form-control,.input-group>.form-control+.custom-select,.input-group>.form-control+.custom-file,.input-group>.form-control-plaintext+.form-control,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.custom-file,.input-group>.custom-select+.form-control,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.custom-file,.input-group>.custom-file+.form-control,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.custom-file{margin-left:-1px}.input-group>.form-control:focus,.input-group>.custom-select:focus,.input-group>.custom-file .custom-file-input:focus ~ .custom-file-label{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.form-control:not(:last-child),.input-group>.custom-select:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.form-control:not(:first-child),.input-group>.custom-select:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-prepend,.input-group-append{display:-ms-flexbox;display:flex}.input-group-prepend .btn,.input-group-append .btn{position:relative;z-index:2}.input-group-prepend .btn:focus,.input-group-append .btn:focus{z-index:3}.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.input-group-text,.input-group-append .input-group-text+.btn{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#6b778c;text-align:center;white-space:nowrap;background-color:#ebecf0;border:1px solid #c1c7d0;border-radius:.25rem}.input-group-text input[type="radio"],.input-group-text input[type="checkbox"]{margin-top:0}.input-group-lg>.form-control:not(textarea),.input-group-lg>.custom-select{height:calc(1.5em + 1rem + 2px)}.input-group-lg>.form-control,.input-group-lg>.custom-select,.input-group-lg>.input-group-prepend>.input-group-text,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-append>.btn{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.form-control:not(textarea),.input-group-sm>.custom-select{height:calc(1.5em + .5rem + 2px)}.input-group-sm>.form-control,.input-group-sm>.custom-select,.input-group-sm>.input-group-prepend>.input-group-text,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-append>.btn{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text,.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked ~ .custom-control-label::before{color:#fff;border-color:#091e42;background-color:#091e42}.custom-control-input:focus ~ .custom-control-label::before{box-shadow:0 0 0 .2rem rgba(0,82,204,0.15)}.custom-control-input:focus:not(:checked) ~ .custom-control-label::before{border-color:#1851b2}.custom-control-input:not(:disabled):active ~ .custom-control-label::before{color:#fff;background-color:#1e65df;border-color:#1e65df}.custom-control-input:disabled ~ .custom-control-label{color:#a5adba}.custom-control-input:disabled ~ .custom-control-label::before{background-color:#ebecf0}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#b3bac5 solid 1px}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:no-repeat 50% / 50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before{border-color:#091e42;background-color:#091e42}.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before{background-color:rgba(101,84,192,0.5)}.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before{background-color:rgba(101,84,192,0.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked ~ .custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before{background-color:rgba(101,84,192,0.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#b3bac5;border-radius:.5rem;transition:background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out,-webkit-transform 0.15s ease-in-out;transition:transform 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out;transition:transform 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out,-webkit-transform 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked ~ .custom-control-label::after{background-color:#fff;-webkit-transform:translateX(.75rem);transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before{background-color:rgba(101,84,192,0.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#6b778c;vertical-align:middle;background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23505f79' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px;background-color:#fff;border:1px solid #c1c7d0;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#1851b2;outline:0;box-shadow:0 0 0 .2rem rgba(0,82,204,0.15)}.custom-select:focus::-ms-value{color:#6b778c;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#a5adba;background-color:#ebecf0}.custom-select::-ms-expand{display:none}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + .75rem + 2px);margin:0;opacity:0}.custom-file-input:focus ~ .custom-file-label{border-color:#1851b2;box-shadow:0 0 0 .2rem rgba(0,82,204,0.15)}.custom-file-input:disabled ~ .custom-file-label{background-color:#ebecf0}.custom-file-input:lang(en) ~ .custom-file-label::after{content:"Browse"}.custom-file-input ~ .custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-weight:400;line-height:1.5;color:#6b778c;background-color:#fff;border:1px solid #c1c7d0;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);padding:.375rem .75rem;line-height:1.5;color:#6b778c;content:"Browse";background-color:#ebecf0;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:calc(1rem + .4rem);padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:none}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #f4f5f7,0 0 0 .2rem rgba(0,82,204,0.15)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #f4f5f7,0 0 0 .2rem rgba(0,82,204,0.15)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #f4f5f7,0 0 0 .2rem rgba(0,82,204,0.15)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#091e42;border:0;border-radius:1rem;transition:background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-webkit-slider-thumb{transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#1e65df}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dfe1e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#091e42;border:0;border-radius:1rem;transition:background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-moz-range-thumb{transition:none}}.custom-range::-moz-range-thumb:active{background-color:#1e65df}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dfe1e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#091e42;border:0;border-radius:1rem;transition:background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out;appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-ms-thumb{transition:none}}.custom-range::-ms-thumb:active{background-color:#1e65df}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dfe1e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dfe1e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#b3bac5}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#b3bac5}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#b3bac5}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:hover,.nav-link:focus{text-decoration:none}.nav-link.disabled{color:#a5adba;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dfe1e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:hover,.nav-tabs .nav-link:focus{border-color:#ebecf0 #ebecf0 #dfe1e6}.nav-tabs .nav-link.disabled{color:#a5adba;background-color:transparent;border-color:transparent}.nav-tabs .nav-link.active,.nav-tabs .nav-item.show .nav-link{color:#6b778c;background-color:#f4f5f7;border-color:#dfe1e6 #dfe1e6 #f4f5f7}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#091e42}.nav-fill .nav-item{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar>.container,.navbar>.container-fluid{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:hover,.navbar-toggler:focus{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width: 575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{padding-right:0;padding-left:0}}@media (min-width: 576px){.navbar-expand-sm{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width: 767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{padding-right:0;padding-left:0}}@media (min-width: 768px){.navbar-expand-md{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width: 991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{padding-right:0;padding-left:0}}@media (min-width: 992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width: 1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{padding-right:0;padding-left:0}}@media (min-width: 1200px){.navbar-expand-xl{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(9,30,66,0.9)}.navbar-light .navbar-brand:hover,.navbar-light .navbar-brand:focus{color:rgba(9,30,66,0.9)}.navbar-light .navbar-nav .nav-link{color:rgba(9,30,66,0.5)}.navbar-light .navbar-nav .nav-link:hover,.navbar-light .navbar-nav .nav-link:focus{color:rgba(9,30,66,0.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(9,30,66,0.3)}.navbar-light .navbar-nav .show>.nav-link,.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .nav-link.active{color:rgba(9,30,66,0.9)}.navbar-light .navbar-toggler{color:rgba(9,30,66,0.5);border-color:rgba(9,30,66,0.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(9,30,66,0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(9,30,66,0.5)}.navbar-light .navbar-text a{color:rgba(9,30,66,0.9)}.navbar-light .navbar-text a:hover,.navbar-light .navbar-text a:focus{color:rgba(9,30,66,0.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:hover,.navbar-dark .navbar-brand:focus{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,0.5)}.navbar-dark .navbar-nav .nav-link:hover,.navbar-dark .navbar-nav .nav-link:focus{color:rgba(255,255,255,0.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,0.25)}.navbar-dark .navbar-nav .show>.nav-link,.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .nav-link.active{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,0.5);border-color:rgba(255,255,255,0.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(255,255,255,0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,0.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:hover,.navbar-dark .navbar-text a:focus{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(9,30,66,0.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-body,.card-admin-form .form-fieldset{-ms-flex:1 1 auto;flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,0);border-bottom:1px solid rgba(9,30,66,0.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,0);border-top:1px solid rgba(9,30,66,0.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img{width:100%;border-radius:calc(.25rem - 1px)}.card-img-top{width:100%;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img-bottom{width:100%;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-deck .card{margin-bottom:15px}@media (min-width: 576px){.card-deck{-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{display:-ms-flexbox;display:flex;-ms-flex:1 0 0%;flex:1 0 0%;-ms-flex-direction:column;flex-direction:column;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-group>.card{margin-bottom:15px}@media (min-width: 576px){.card-group{-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-img-top,.card-group>.card:not(:last-child) .card-header{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-img-bottom,.card-group>.card:not(:last-child) .card-footer{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-img-top,.card-group>.card:not(:first-child) .card-header{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-img-bottom,.card-group>.card:not(:first-child) .card-footer{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width: 576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion>.card{overflow:hidden}.accordion>.card:not(:first-of-type) .card-header:first-child{border-radius:0}.accordion>.card:not(:first-of-type):not(:last-of-type){border-bottom:0;border-radius:0}.accordion>.card:first-of-type{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:last-of-type{border-top-left-radius:0;border-top-right-radius:0}.accordion>.card .card-header{margin-bottom:-1px}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#ebecf0;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;color:#a5adba;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#a5adba}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#6b778c;background-color:#fff;border:1px solid #dfe1e6}.page-link:hover{z-index:2;color:#172b4d;text-decoration:none;background-color:#ebecf0;border-color:#dfe1e6}.page-link:focus{z-index:2;outline:0;box-shadow:0 0 0 .2rem rgba(0,82,204,0.15)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:1;color:#fff;background-color:#091e42;border-color:#091e42}.page-item.disabled .page-link{color:#a5adba;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dfe1e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.badge{transition:none}}a.badge:hover,a.badge:focus{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#6554c0}a.badge-primary:hover,a.badge-primary:focus{color:#fff;background-color:#4d3da4}a.badge-primary:focus,a.badge-primary.focus{outline:0;box-shadow:0 0 0 .2rem rgba(101,84,192,0.5)}.badge-secondary{color:#172b4d;background-color:#a5adba}a.badge-secondary:hover,a.badge-secondary:focus{color:#172b4d;background-color:#8893a4}a.badge-secondary:focus,a.badge-secondary.focus{outline:0;box-shadow:0 0 0 .2rem rgba(165,173,186,0.5)}.badge-success{color:#fff;background-color:#36b37e}a.badge-success:hover,a.badge-success:focus{color:#fff;background-color:#2a8c62}a.badge-success:focus,a.badge-success.focus{outline:0;box-shadow:0 0 0 .2rem rgba(54,179,126,0.5)}.badge-info{color:#fff;background-color:#00b8d9}a.badge-info:hover,a.badge-info:focus{color:#fff;background-color:#008da6}a.badge-info:focus,a.badge-info.focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,184,217,0.5)}.badge-warning{color:#172b4d;background-color:#ffc107}a.badge-warning:hover,a.badge-warning:focus{color:#172b4d;background-color:#d39e00}a.badge-warning:focus,a.badge-warning.focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,193,7,0.5)}.badge-danger{color:#fff;background-color:#ff5630}a.badge-danger:hover,a.badge-danger:focus{color:#fff;background-color:#fc2e00}a.badge-danger:focus,a.badge-danger.focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,86,48,0.5)}.badge-light{color:#172b4d;background-color:#ebecf0}a.badge-light:hover,a.badge-light:focus{color:#172b4d;background-color:#ced0da}a.badge-light:focus,a.badge-light.focus{outline:0;box-shadow:0 0 0 .2rem rgba(235,236,240,0.5)}.badge-dark{color:#fff;background-color:#505f79}a.badge-dark:hover,a.badge-dark:focus{color:#fff;background-color:#3c475a}a.badge-dark:focus,a.badge-dark.focus{outline:0;box-shadow:0 0 0 .2rem rgba(80,95,121,0.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#ebecf0;border-radius:.3rem}@media (min-width: 576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:0 solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#2a316f;background-color:#e0ddf2;border-color:#d4cfed}.alert-primary hr{border-top-color:#c3bce6}.alert-primary .alert-link{color:#1c214a}.alert-secondary{color:#41516d;background-color:#edeff1;border-color:#e6e8ec}.alert-secondary hr{border-top-color:#d8dbe1}.alert-secondary .alert-link{color:#2e394d}.alert-success{color:#195458;background-color:#d7f0e5;border-color:#c7eadb}.alert-success hr{border-top-color:#b4e3cf}.alert-success .alert-link{color:#0e2e30}.alert-info{color:#065578;background-color:#ccf1f7;border-color:#b8ebf4}.alert-info hr{border-top-color:#a2e5f1}.alert-info .alert-link{color:#043347}.alert-warning{color:#62592d;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#3f391d}.alert-danger{color:#62323c;background-color:#ffddd6;border-color:#ffd0c5}.alert-danger hr{border-top-color:#ffbbac}.alert-danger .alert-link{color:#402127}.alert-light{color:#5a6881;background-color:#fbfbfc;border-color:#f9fafb}.alert-light hr{border-top-color:#eaedf1}.alert-light .alert-link{color:#455063}.alert-dark{color:#233556;background-color:#dcdfe4;border-color:#ced2d9}.alert-dark hr{border-top-color:#c0c5ce}.alert-dark .alert-link{color:#141f32}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#ebecf0;border-radius:.25rem}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;color:#fff;text-align:center;white-space:nowrap;background-color:#6554c0;transition:width 0.6s ease}@media (prefers-reduced-motion: reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion: reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media,.nav-side .nav-link{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#6b778c;text-align:inherit}.list-group-item-action:hover,.list-group-item-action:focus{z-index:1;color:#6b778c;text-decoration:none;background-color:#f4f5f7}.list-group-item-action:active{color:#172b4d;background-color:#ebecf0}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid rgba(9,30,66,0.125)}.list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item.disabled,.list-group-item:disabled{color:#a5adba;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#091e42;border-color:#091e42}.list-group-horizontal{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}@media (min-width: 576px){.list-group-horizontal-sm{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-sm .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}@media (min-width: 768px){.list-group-horizontal-md{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-md .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}@media (min-width: 992px){.list-group-horizontal-lg{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-lg .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}@media (min-width: 1200px){.list-group-horizontal-xl{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-xl .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush .list-group-item:last-child{margin-bottom:-1px}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{margin-bottom:0;border-bottom:0}.list-group-item-primary{color:#393a84;background-color:#d4cfed}.list-group-item-primary.list-group-item-action:hover,.list-group-item-primary.list-group-item-action:focus{color:#393a84;background-color:#c3bce6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#393a84;border-color:#393a84}.list-group-item-secondary{color:#5a6880;background-color:#e6e8ec}.list-group-item-secondary.list-group-item-action:hover,.list-group-item-secondary.list-group-item-action:focus{color:#5a6880;background-color:#d8dbe1}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#5a6880;border-color:#5a6880}.list-group-item-success{color:#206b61;background-color:#c7eadb}.list-group-item-success.list-group-item-action:hover,.list-group-item-success.list-group-item-action:focus{color:#206b61;background-color:#b4e3cf}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#206b61;border-color:#206b61}.list-group-item-info{color:#046e91;background-color:#b8ebf4}.list-group-item-info.list-group-item-action:hover,.list-group-item-info.list-group-item-action:focus{color:#046e91;background-color:#a2e5f1}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#046e91;border-color:#046e91}.list-group-item-warning{color:#897323;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:hover,.list-group-item-warning.list-group-item-action:focus{color:#897323;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#897323;border-color:#897323}.list-group-item-danger{color:#893b39;background-color:#ffd0c5}.list-group-item-danger.list-group-item-action:hover,.list-group-item-danger.list-group-item-action:focus{color:#893b39;background-color:#ffbbac}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#893b39;border-color:#893b39}.list-group-item-light{color:#7f899c;background-color:#f9fafb}.list-group-item-light.list-group-item-action:hover,.list-group-item-light.list-group-item-action:focus{color:#7f899c;background-color:#eaedf1}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#7f899c;border-color:#7f899c}.list-group-item-dark{color:#2e405f;background-color:#ced2d9}.list-group-item-dark.list-group-item-action:hover,.list-group-item-dark.list-group-item-action:focus{color:#2e405f;background-color:#c0c5ce}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#2e405f;border-color:#2e405f}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#091e42;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover{color:#091e42;text-decoration:none}.close:not(:disabled):not(.disabled):hover,.close:not(:disabled):not(.disabled):focus{opacity:.75}button.close{padding:0;background-color:transparent;border:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}a.close.disabled{pointer-events:none}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform 0.3s ease-out;transition:transform 0.3s ease-out;transition:transform 0.3s ease-out, -webkit-transform 0.3s ease-out;-webkit-transform:translate(0, -50px);transform:translate(0, -50px)}@media (prefers-reduced-motion: reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal-dialog-scrollable{display:-ms-flexbox;display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-header,.modal-dialog-scrollable .modal-footer{-ms-flex-negative:0;flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);content:""}.modal-dialog-centered.modal-dialog-scrollable{-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(9,30,66,0.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#091e42}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dfe1e6;border-top-left-radius:.3rem;border-top-right-radius:.3rem}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:1rem;border-top:1px solid #dfe1e6;border-bottom-right-radius:.3rem;border-bottom-left-radius:.3rem}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width: 576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width: 992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width: 1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-top,.bs-tooltip-auto[x-placement^="top"]{padding:.4rem 0}.bs-tooltip-top .arrow,.bs-tooltip-auto[x-placement^="top"] .arrow{bottom:0}.bs-tooltip-top .arrow::before,.bs-tooltip-auto[x-placement^="top"] .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#091e42}.bs-tooltip-right,.bs-tooltip-auto[x-placement^="right"]{padding:0 .4rem}.bs-tooltip-right .arrow,.bs-tooltip-auto[x-placement^="right"] .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-right .arrow::before,.bs-tooltip-auto[x-placement^="right"] .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#091e42}.bs-tooltip-bottom,.bs-tooltip-auto[x-placement^="bottom"]{padding:.4rem 0}.bs-tooltip-bottom .arrow,.bs-tooltip-auto[x-placement^="bottom"] .arrow{top:0}.bs-tooltip-bottom .arrow::before,.bs-tooltip-auto[x-placement^="bottom"] .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#091e42}.bs-tooltip-left,.bs-tooltip-auto[x-placement^="left"]{padding:0 .4rem}.bs-tooltip-left .arrow,.bs-tooltip-auto[x-placement^="left"] .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-left .arrow::before,.bs-tooltip-auto[x-placement^="left"] .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#091e42}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#091e42;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(9,30,66,0.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::before,.popover .arrow::after{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-top,.bs-popover-auto[x-placement^="top"]{margin-bottom:.5rem}.bs-popover-top>.arrow,.bs-popover-auto[x-placement^="top"]>.arrow{bottom:calc((.5rem + 1px) * -1)}.bs-popover-top>.arrow::before,.bs-popover-auto[x-placement^="top"]>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(9,30,66,0.25)}.bs-popover-top>.arrow::after,.bs-popover-auto[x-placement^="top"]>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-right,.bs-popover-auto[x-placement^="right"]{margin-left:.5rem}.bs-popover-right>.arrow,.bs-popover-auto[x-placement^="right"]>.arrow{left:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-right>.arrow::before,.bs-popover-auto[x-placement^="right"]>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(9,30,66,0.25)}.bs-popover-right>.arrow::after,.bs-popover-auto[x-placement^="right"]>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-bottom,.bs-popover-auto[x-placement^="bottom"]{margin-top:.5rem}.bs-popover-bottom>.arrow,.bs-popover-auto[x-placement^="bottom"]>.arrow{top:calc((.5rem + 1px) * -1)}.bs-popover-bottom>.arrow::before,.bs-popover-auto[x-placement^="bottom"]>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(9,30,66,0.25)}.bs-popover-bottom>.arrow::after,.bs-popover-auto[x-placement^="bottom"]>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-bottom .popover-header::before,.bs-popover-auto[x-placement^="bottom"] .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-left,.bs-popover-auto[x-placement^="left"]{margin-right:.5rem}.bs-popover-left>.arrow,.bs-popover-auto[x-placement^="left"]>.arrow{right:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-left>.arrow::before,.bs-popover-auto[x-placement^="left"]>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(9,30,66,0.25)}.bs-popover-left>.arrow::after,.bs-popover-auto[x-placement^="left"]>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#172b4d}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:spinner-border .75s linear infinite;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:spinner-grow .75s linear infinite;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}.align-baseline{vertical-align:baseline !important}.align-top{vertical-align:top !important}.align-middle{vertical-align:middle !important}.align-bottom{vertical-align:bottom !important}.align-text-bottom{vertical-align:text-bottom !important}.align-text-top{vertical-align:text-top !important}.bg-primary{background-color:#6554c0 !important}a.bg-primary:hover,a.bg-primary:focus,button.bg-primary:hover,button.bg-primary:focus{background-color:#4d3da4 !important}.bg-secondary{background-color:#a5adba !important}a.bg-secondary:hover,a.bg-secondary:focus,button.bg-secondary:hover,button.bg-secondary:focus{background-color:#8893a4 !important}.bg-success{background-color:#36b37e !important}a.bg-success:hover,a.bg-success:focus,button.bg-success:hover,button.bg-success:focus{background-color:#2a8c62 !important}.bg-info{background-color:#00b8d9 !important}a.bg-info:hover,a.bg-info:focus,button.bg-info:hover,button.bg-info:focus{background-color:#008da6 !important}.bg-warning{background-color:#ffc107 !important}a.bg-warning:hover,a.bg-warning:focus,button.bg-warning:hover,button.bg-warning:focus{background-color:#d39e00 !important}.bg-danger{background-color:#ff5630 !important}a.bg-danger:hover,a.bg-danger:focus,button.bg-danger:hover,button.bg-danger:focus{background-color:#fc2e00 !important}.bg-light{background-color:#ebecf0 !important}a.bg-light:hover,a.bg-light:focus,button.bg-light:hover,button.bg-light:focus{background-color:#ced0da !important}.bg-dark{background-color:#505f79 !important}a.bg-dark:hover,a.bg-dark:focus,button.bg-dark:hover,button.bg-dark:focus{background-color:#3c475a !important}.bg-white{background-color:#fff !important}.bg-transparent{background-color:transparent !important}.border,.control-month-picker,.control-time-picker,.control-image-preview{border:1px solid #dfe1e6 !important}.border-top,.card-admin-form .form-fieldset+.form-fieldset{border-top:1px solid #dfe1e6 !important}.border-right{border-right:1px solid #dfe1e6 !important}.border-bottom{border-bottom:1px solid #dfe1e6 !important}.border-left,.page-header h1 small{border-left:1px solid #dfe1e6 !important}.border-0,.control-time-picker input{border:0 !important}.border-top-0,.card-admin-table>:first-child th{border-top:0 !important}.border-right-0{border-right:0 !important}.border-bottom-0,.card-admin-table th{border-bottom:0 !important}.border-left-0{border-left:0 !important}.border-primary{border-color:#6554c0 !important}.border-secondary{border-color:#a5adba !important}.border-success{border-color:#36b37e !important}.border-info{border-color:#00b8d9 !important}.border-warning{border-color:#ffc107 !important}.border-danger,.card-admin-error,.card-admin-error .card-header,.login-error-card{border-color:#ff5630 !important}.border-light{border-color:#ebecf0 !important}.border-dark{border-color:#505f79 !important}.border-white{border-color:#fff !important}.rounded-sm{border-radius:.2rem !important}.rounded,.control-month-picker,.control-time-picker,.control-image-preview,.card-admin-settings-card,.media-check-icon{border-radius:.25rem !important}.rounded-top{border-top-left-radius:.25rem !important;border-top-right-radius:.25rem !important}.rounded-right{border-top-right-radius:.25rem !important;border-bottom-right-radius:.25rem !important}.rounded-bottom{border-bottom-right-radius:.25rem !important;border-bottom-left-radius:.25rem !important}.rounded-left{border-top-left-radius:.25rem !important;border-bottom-left-radius:.25rem !important}.rounded-lg,.card-admin-table img{border-radius:.3rem !important}.rounded-circle{border-radius:50% !important}.rounded-pill{border-radius:50rem !important}.rounded-0{border-radius:0 !important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none !important}.d-inline{display:inline !important}.d-inline-block,.control-image-preview{display:inline-block !important}.d-block,.card-admin-settings-card{display:block !important}.d-table{display:table !important}.d-table-row{display:table-row !important}.d-table-cell{display:table-cell !important}.d-flex,.control-time-picker,.page-header h1,.media-check-icon{display:-ms-flexbox !important;display:flex !important}.d-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}@media (min-width: 576px){.d-sm-none{display:none !important}.d-sm-inline{display:inline !important}.d-sm-inline-block{display:inline-block !important}.d-sm-block{display:block !important}.d-sm-table{display:table !important}.d-sm-table-row{display:table-row !important}.d-sm-table-cell{display:table-cell !important}.d-sm-flex{display:-ms-flexbox !important;display:flex !important}.d-sm-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media (min-width: 768px){.d-md-none{display:none !important}.d-md-inline{display:inline !important}.d-md-inline-block{display:inline-block !important}.d-md-block{display:block !important}.d-md-table{display:table !important}.d-md-table-row{display:table-row !important}.d-md-table-cell{display:table-cell !important}.d-md-flex{display:-ms-flexbox !important;display:flex !important}.d-md-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media (min-width: 992px){.d-lg-none{display:none !important}.d-lg-inline{display:inline !important}.d-lg-inline-block{display:inline-block !important}.d-lg-block{display:block !important}.d-lg-table{display:table !important}.d-lg-table-row{display:table-row !important}.d-lg-table-cell{display:table-cell !important}.d-lg-flex{display:-ms-flexbox !important;display:flex !important}.d-lg-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media (min-width: 1200px){.d-xl-none{display:none !important}.d-xl-inline{display:inline !important}.d-xl-inline-block{display:inline-block !important}.d-xl-block{display:block !important}.d-xl-table{display:table !important}.d-xl-table-row{display:table-row !important}.d-xl-table-cell{display:table-cell !important}.d-xl-flex{display:-ms-flexbox !important;display:flex !important}.d-xl-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media print{.d-print-none{display:none !important}.d-print-inline{display:inline !important}.d-print-inline-block{display:inline-block !important}.d-print-block{display:block !important}.d-print-table{display:table !important}.d-print-table-row{display:table-row !important}.d-print-table-cell{display:table-cell !important}.d-print-flex{display:-ms-flexbox !important;display:flex !important}.d-print-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.85714%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row !important;flex-direction:row !important}.flex-column,.nav-side{-ms-flex-direction:column !important;flex-direction:column !important}.flex-row-reverse{-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-column-reverse{-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-fill{-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-grow-0{-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-grow-1{-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-start{-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-end{-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-center,.media-check-icon{-ms-flex-pack:center !important;justify-content:center !important}.justify-content-between{-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-start{-ms-flex-align:start !important;align-items:flex-start !important}.align-items-end{-ms-flex-align:end !important;align-items:flex-end !important}.align-items-center,.control-time-picker,.page-header h1,.media-check-icon{-ms-flex-align:center !important;align-items:center !important}.align-items-baseline{-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-stretch{-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}@media (min-width: 576px){.flex-sm-row{-ms-flex-direction:row !important;flex-direction:row !important}.flex-sm-column{-ms-flex-direction:column !important;flex-direction:column !important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-sm-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-sm-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-sm-fill{-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-sm-grow-0{-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-sm-grow-1{-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-sm-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-sm-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-sm-start{-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-sm-end{-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-sm-center{-ms-flex-pack:center !important;justify-content:center !important}.justify-content-sm-between{-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-sm-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-sm-start{-ms-flex-align:start !important;align-items:flex-start !important}.align-items-sm-end{-ms-flex-align:end !important;align-items:flex-end !important}.align-items-sm-center{-ms-flex-align:center !important;align-items:center !important}.align-items-sm-baseline{-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-sm-stretch{-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-sm-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-sm-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-sm-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-sm-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-sm-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-sm-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-sm-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-sm-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-sm-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-sm-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-sm-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-sm-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}@media (min-width: 768px){.flex-md-row{-ms-flex-direction:row !important;flex-direction:row !important}.flex-md-column{-ms-flex-direction:column !important;flex-direction:column !important}.flex-md-row-reverse{-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-md-column-reverse{-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-md-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-md-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-md-fill{-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-md-grow-0{-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-md-grow-1{-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-md-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-md-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-md-start{-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-md-end{-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-md-center{-ms-flex-pack:center !important;justify-content:center !important}.justify-content-md-between{-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-md-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-md-start{-ms-flex-align:start !important;align-items:flex-start !important}.align-items-md-end{-ms-flex-align:end !important;align-items:flex-end !important}.align-items-md-center{-ms-flex-align:center !important;align-items:center !important}.align-items-md-baseline{-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-md-stretch{-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-md-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-md-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-md-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-md-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-md-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-md-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-md-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-md-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-md-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-md-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-md-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-md-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}@media (min-width: 992px){.flex-lg-row{-ms-flex-direction:row !important;flex-direction:row !important}.flex-lg-column{-ms-flex-direction:column !important;flex-direction:column !important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-lg-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-lg-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-lg-fill{-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-lg-grow-0{-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-lg-grow-1{-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-lg-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-lg-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-lg-start{-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-lg-end{-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-lg-center{-ms-flex-pack:center !important;justify-content:center !important}.justify-content-lg-between{-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-lg-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-lg-start{-ms-flex-align:start !important;align-items:flex-start !important}.align-items-lg-end{-ms-flex-align:end !important;align-items:flex-end !important}.align-items-lg-center{-ms-flex-align:center !important;align-items:center !important}.align-items-lg-baseline{-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-lg-stretch{-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-lg-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-lg-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-lg-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-lg-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-lg-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-lg-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-lg-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-lg-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-lg-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-lg-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-lg-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-lg-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}@media (min-width: 1200px){.flex-xl-row{-ms-flex-direction:row !important;flex-direction:row !important}.flex-xl-column{-ms-flex-direction:column !important;flex-direction:column !important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-xl-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-xl-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-xl-fill{-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-xl-grow-0{-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-xl-grow-1{-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-xl-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-xl-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-xl-start{-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-xl-end{-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-xl-center{-ms-flex-pack:center !important;justify-content:center !important}.justify-content-xl-between{-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-xl-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-xl-start{-ms-flex-align:start !important;align-items:flex-start !important}.align-items-xl-end{-ms-flex-align:end !important;align-items:flex-end !important}.align-items-xl-center{-ms-flex-align:center !important;align-items:center !important}.align-items-xl-baseline{-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-xl-stretch{-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-xl-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-xl-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-xl-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-xl-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-xl-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-xl-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-xl-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-xl-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-xl-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-xl-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-xl-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-xl-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}.float-left{float:left !important}.float-right{float:right !important}.float-none{float:none !important}@media (min-width: 576px){.float-sm-left{float:left !important}.float-sm-right{float:right !important}.float-sm-none{float:none !important}}@media (min-width: 768px){.float-md-left{float:left !important}.float-md-right{float:right !important}.float-md-none{float:none !important}}@media (min-width: 992px){.float-lg-left{float:left !important}.float-lg-right{float:right !important}.float-lg-none{float:none !important}}@media (min-width: 1200px){.float-xl-left{float:left !important}.float-xl-right{float:right !important}.float-xl-none{float:none !important}}.overflow-auto{overflow:auto !important}.overflow-hidden{overflow:hidden !important}.position-static{position:static !important}.position-relative{position:relative !important}.position-absolute{position:absolute !important}.position-fixed{position:fixed !important}.position-sticky{position:-webkit-sticky !important;position:sticky !important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position: -webkit-sticky) or (position: sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm,.login-form-card,.card-admin-settings-grid,.card-admin-info,.card-admin-table,.card-admin-form,.card-admin-error,.login-error-card{box-shadow:0 0.125rem 0.25rem rgba(9,30,66,0.075) !important}.shadow{box-shadow:0 0.5rem 1rem rgba(9,30,66,0.15) !important}.shadow-lg{box-shadow:0 1rem 3rem rgba(9,30,66,0.175) !important}.shadow-none{box-shadow:none !important}.w-25{width:25% !important}.w-50{width:50% !important}.w-75{width:75% !important}.w-100,.card-admin-table .row-select label{width:100% !important}.w-auto{width:auto !important}.h-25{height:25% !important}.h-50{height:50% !important}.h-75{height:75% !important}.h-100{height:100% !important}.h-auto{height:auto !important}.mw-100{max-width:100% !important}.mh-100{max-height:100% !important}.min-vw-100{min-width:100vw !important}.min-vh-100{min-height:100vh !important}.vw-100{width:100vw !important}.vh-100{height:100vh !important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.m-0,.page-header h1,.card-admin-info .card-title,.card-admin-table .card-title,.card-admin-table table,.card-admin-table h5,.card-admin-table .row-select label{margin:0 !important}.mt-0,.media-admin-check h5,.my-0{margin-top:0 !important}.mr-0,.mx-0{margin-right:0 !important}.mb-0,.card-admin-form .form-group:last-child,.card-admin-form .form-fieldset .form-group:last-child,.my-0{margin-bottom:0 !important}.ml-0,.mx-0{margin-left:0 !important}.m-1,.card-admin-settings-card{margin:.25rem !important}.mt-1,.control-checkboxselect .form-check+.form-check,.control-radioselect .form-check+.form-check,.control-image-preview div,.my-1{margin-top:.25rem !important}.mr-1,.page-action .fa,.page-action .fab,.mx-1{margin-right:.25rem !important}.mb-1,.card-admin-settings-card h5,.my-1{margin-bottom:.25rem !important}.ml-1,.mx-1{margin-left:.25rem !important}.m-2{margin:.5rem !important}.mt-2,.my-2{margin-top:.5rem !important}.mr-2,.mx-2{margin-right:.5rem !important}.mb-2,.control-image-preview,.my-2{margin-bottom:.5rem !important}.ml-2,.control-image-preview div span+span,.mx-2{margin-left:.5rem !important}.m-3{margin:1rem !important}.mt-3,.my-3{margin-top:1rem !important}.mr-3,.media-check-icon,.mx-3{margin-right:1rem !important}.mb-3,.page-header,.card-admin-info,.card-admin-form .card-admin-table,.my-3{margin-bottom:1rem !important}.ml-3,.control-yesno-switch .radio+.radio,.page-header h1 small,.mx-3{margin-left:1rem !important}.m-4{margin:1.5rem !important}.mt-4,.my-4{margin-top:1.5rem !important}.mr-4,.mx-4{margin-right:1.5rem !important}.mb-4,.my-4{margin-bottom:1.5rem !important}.ml-4,.mx-4{margin-left:1.5rem !important}.m-5{margin:3rem !important}.mt-5,.my-5{margin-top:3rem !important}.mr-5,.mx-5{margin-right:3rem !important}.mb-5,.my-5{margin-bottom:3rem !important}.ml-5,.mx-5{margin-left:3rem !important}.p-0,.navbar .btn-user{padding:0 !important}.pt-0,.card-admin-form .form-fieldset:first-child,.py-0{padding-top:0 !important}.pr-0,.px-0{padding-right:0 !important}.pb-0,.control-image-preview.control-image-metadata,.card-admin-form .form-fieldset:last-child,.py-0{padding-bottom:0 !important}.pl-0,.card-admin-table .badges-list,.px-0{padding-left:0 !important}.p-1,.control-month-picker,.control-time-picker{padding:.25rem !important}.pt-1,.py-1,.page-header h1 small,.card-admin-settings-grid .card-body,.card-admin-settings-grid .card-admin-form .form-fieldset,.card-admin-form .card-admin-settings-grid .form-fieldset,.card-admin-table th,.card-admin-table td{padding-top:.25rem !important}.pr-1,.px-1{padding-right:.25rem !important}.pb-1,.py-1,.page-header h1 small,.card-admin-settings-grid .card-body,.card-admin-settings-grid .card-admin-form .form-fieldset,.card-admin-form .card-admin-settings-grid .form-fieldset,.card-admin-table th,.card-admin-table td{padding-bottom:.25rem !important}.pl-1,.nav-side .media-body,.px-1{padding-left:.25rem !important}.p-2,.card-admin-settings-card,.card-admin-table .row-select label{padding:.5rem !important}.pt-2,.py-2,.control-image-preview div{padding-top:.5rem !important}.pr-2,.px-2{padding-right:.5rem !important}.pb-2,.py-2,.control-image-preview div{padding-bottom:.5rem !important}.pl-2,.px-2{padding-left:.5rem !important}.p-3,.control-image-preview{padding:1rem !important}.pt-3,.py-3,.card-admin-analytics-summary,.card-admin-table .blankslate td{padding-top:1rem !important}.pr-3,.px-3{padding-right:1rem !important}.pb-3,.py-3,.card-admin-analytics-summary,.card-admin-table .blankslate td{padding-bottom:1rem !important}.pl-3,.page-header h1 small,.px-3{padding-left:1rem !important}.p-4{padding:1.5rem !important}.pt-4,.py-4{padding-top:1.5rem !important}.pr-4,.px-4{padding-right:1.5rem !important}.pb-4,.py-4{padding-bottom:1.5rem !important}.pl-4,.px-4{padding-left:1.5rem !important}.p-5{padding:3rem !important}.pt-5,.py-5{padding-top:3rem !important}.pr-5,.px-5{padding-right:3rem !important}.pb-5,.py-5{padding-bottom:3rem !important}.pl-5,.px-5{padding-left:3rem !important}.m-n1{margin:-.25rem !important}.mt-n1,.my-n1{margin-top:-.25rem !important}.mr-n1,.mx-n1{margin-right:-.25rem !important}.mb-n1,.my-n1{margin-bottom:-.25rem !important}.ml-n1,.mx-n1{margin-left:-.25rem !important}.m-n2{margin:-.5rem !important}.mt-n2,.my-n2{margin-top:-.5rem !important}.mr-n2,.mx-n2{margin-right:-.5rem !important}.mb-n2,.my-n2{margin-bottom:-.5rem !important}.ml-n2,.mx-n2{margin-left:-.5rem !important}.m-n3{margin:-1rem !important}.mt-n3,.my-n3{margin-top:-1rem !important}.mr-n3,.mx-n3{margin-right:-1rem !important}.mb-n3,.my-n3{margin-bottom:-1rem !important}.ml-n3,.mx-n3{margin-left:-1rem !important}.m-n4{margin:-1.5rem !important}.mt-n4,.my-n4{margin-top:-1.5rem !important}.mr-n4,.mx-n4{margin-right:-1.5rem !important}.mb-n4,.my-n4{margin-bottom:-1.5rem !important}.ml-n4,.mx-n4{margin-left:-1.5rem !important}.m-n5{margin:-3rem !important}.mt-n5,.my-n5{margin-top:-3rem !important}.mr-n5,.mx-n5{margin-right:-3rem !important}.mb-n5,.my-n5{margin-bottom:-3rem !important}.ml-n5,.mx-n5{margin-left:-3rem !important}.m-auto{margin:auto !important}.mt-auto,.my-auto{margin-top:auto !important}.mr-auto,.mx-auto{margin-right:auto !important}.mb-auto,.my-auto{margin-bottom:auto !important}.ml-auto,.mx-auto{margin-left:auto !important}@media (min-width: 576px){.m-sm-0{margin:0 !important}.mt-sm-0,.my-sm-0{margin-top:0 !important}.mr-sm-0,.mx-sm-0{margin-right:0 !important}.mb-sm-0,.my-sm-0{margin-bottom:0 !important}.ml-sm-0,.mx-sm-0{margin-left:0 !important}.m-sm-1{margin:.25rem !important}.mt-sm-1,.my-sm-1{margin-top:.25rem !important}.mr-sm-1,.mx-sm-1{margin-right:.25rem !important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem !important}.ml-sm-1,.mx-sm-1{margin-left:.25rem !important}.m-sm-2{margin:.5rem !important}.mt-sm-2,.my-sm-2{margin-top:.5rem !important}.mr-sm-2,.mx-sm-2{margin-right:.5rem !important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem !important}.ml-sm-2,.mx-sm-2{margin-left:.5rem !important}.m-sm-3{margin:1rem !important}.mt-sm-3,.my-sm-3{margin-top:1rem !important}.mr-sm-3,.mx-sm-3{margin-right:1rem !important}.mb-sm-3,.my-sm-3{margin-bottom:1rem !important}.ml-sm-3,.mx-sm-3{margin-left:1rem !important}.m-sm-4{margin:1.5rem !important}.mt-sm-4,.my-sm-4{margin-top:1.5rem !important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem !important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem !important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem !important}.m-sm-5{margin:3rem !important}.mt-sm-5,.my-sm-5{margin-top:3rem !important}.mr-sm-5,.mx-sm-5{margin-right:3rem !important}.mb-sm-5,.my-sm-5{margin-bottom:3rem !important}.ml-sm-5,.mx-sm-5{margin-left:3rem !important}.p-sm-0{padding:0 !important}.pt-sm-0,.py-sm-0{padding-top:0 !important}.pr-sm-0,.px-sm-0{padding-right:0 !important}.pb-sm-0,.py-sm-0{padding-bottom:0 !important}.pl-sm-0,.px-sm-0{padding-left:0 !important}.p-sm-1{padding:.25rem !important}.pt-sm-1,.py-sm-1{padding-top:.25rem !important}.pr-sm-1,.px-sm-1{padding-right:.25rem !important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem !important}.pl-sm-1,.px-sm-1{padding-left:.25rem !important}.p-sm-2{padding:.5rem !important}.pt-sm-2,.py-sm-2{padding-top:.5rem !important}.pr-sm-2,.px-sm-2{padding-right:.5rem !important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem !important}.pl-sm-2,.px-sm-2{padding-left:.5rem !important}.p-sm-3{padding:1rem !important}.pt-sm-3,.py-sm-3{padding-top:1rem !important}.pr-sm-3,.px-sm-3{padding-right:1rem !important}.pb-sm-3,.py-sm-3{padding-bottom:1rem !important}.pl-sm-3,.px-sm-3{padding-left:1rem !important}.p-sm-4{padding:1.5rem !important}.pt-sm-4,.py-sm-4{padding-top:1.5rem !important}.pr-sm-4,.px-sm-4{padding-right:1.5rem !important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem !important}.pl-sm-4,.px-sm-4{padding-left:1.5rem !important}.p-sm-5{padding:3rem !important}.pt-sm-5,.py-sm-5{padding-top:3rem !important}.pr-sm-5,.px-sm-5{padding-right:3rem !important}.pb-sm-5,.py-sm-5{padding-bottom:3rem !important}.pl-sm-5,.px-sm-5{padding-left:3rem !important}.m-sm-n1{margin:-.25rem !important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem !important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem !important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem !important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem !important}.m-sm-n2{margin:-.5rem !important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem !important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem !important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem !important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem !important}.m-sm-n3{margin:-1rem !important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem !important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem !important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem !important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem !important}.m-sm-n4{margin:-1.5rem !important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem !important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem !important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem !important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem !important}.m-sm-n5{margin:-3rem !important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem !important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem !important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem !important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem !important}.m-sm-auto{margin:auto !important}.mt-sm-auto,.my-sm-auto{margin-top:auto !important}.mr-sm-auto,.mx-sm-auto{margin-right:auto !important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto !important}.ml-sm-auto,.mx-sm-auto{margin-left:auto !important}}@media (min-width: 768px){.m-md-0{margin:0 !important}.mt-md-0,.my-md-0{margin-top:0 !important}.mr-md-0,.mx-md-0{margin-right:0 !important}.mb-md-0,.my-md-0{margin-bottom:0 !important}.ml-md-0,.mx-md-0{margin-left:0 !important}.m-md-1{margin:.25rem !important}.mt-md-1,.my-md-1{margin-top:.25rem !important}.mr-md-1,.mx-md-1{margin-right:.25rem !important}.mb-md-1,.my-md-1{margin-bottom:.25rem !important}.ml-md-1,.mx-md-1{margin-left:.25rem !important}.m-md-2{margin:.5rem !important}.mt-md-2,.my-md-2{margin-top:.5rem !important}.mr-md-2,.mx-md-2{margin-right:.5rem !important}.mb-md-2,.my-md-2{margin-bottom:.5rem !important}.ml-md-2,.mx-md-2{margin-left:.5rem !important}.m-md-3{margin:1rem !important}.mt-md-3,.my-md-3{margin-top:1rem !important}.mr-md-3,.mx-md-3{margin-right:1rem !important}.mb-md-3,.my-md-3{margin-bottom:1rem !important}.ml-md-3,.mx-md-3{margin-left:1rem !important}.m-md-4{margin:1.5rem !important}.mt-md-4,.my-md-4{margin-top:1.5rem !important}.mr-md-4,.mx-md-4{margin-right:1.5rem !important}.mb-md-4,.my-md-4{margin-bottom:1.5rem !important}.ml-md-4,.mx-md-4{margin-left:1.5rem !important}.m-md-5{margin:3rem !important}.mt-md-5,.my-md-5{margin-top:3rem !important}.mr-md-5,.mx-md-5{margin-right:3rem !important}.mb-md-5,.my-md-5{margin-bottom:3rem !important}.ml-md-5,.mx-md-5{margin-left:3rem !important}.p-md-0{padding:0 !important}.pt-md-0,.py-md-0{padding-top:0 !important}.pr-md-0,.px-md-0{padding-right:0 !important}.pb-md-0,.py-md-0{padding-bottom:0 !important}.pl-md-0,.px-md-0{padding-left:0 !important}.p-md-1{padding:.25rem !important}.pt-md-1,.py-md-1{padding-top:.25rem !important}.pr-md-1,.px-md-1{padding-right:.25rem !important}.pb-md-1,.py-md-1{padding-bottom:.25rem !important}.pl-md-1,.px-md-1{padding-left:.25rem !important}.p-md-2{padding:.5rem !important}.pt-md-2,.py-md-2{padding-top:.5rem !important}.pr-md-2,.px-md-2{padding-right:.5rem !important}.pb-md-2,.py-md-2{padding-bottom:.5rem !important}.pl-md-2,.px-md-2{padding-left:.5rem !important}.p-md-3{padding:1rem !important}.pt-md-3,.py-md-3{padding-top:1rem !important}.pr-md-3,.px-md-3{padding-right:1rem !important}.pb-md-3,.py-md-3{padding-bottom:1rem !important}.pl-md-3,.px-md-3{padding-left:1rem !important}.p-md-4{padding:1.5rem !important}.pt-md-4,.py-md-4{padding-top:1.5rem !important}.pr-md-4,.px-md-4{padding-right:1.5rem !important}.pb-md-4,.py-md-4{padding-bottom:1.5rem !important}.pl-md-4,.px-md-4{padding-left:1.5rem !important}.p-md-5{padding:3rem !important}.pt-md-5,.py-md-5{padding-top:3rem !important}.pr-md-5,.px-md-5{padding-right:3rem !important}.pb-md-5,.py-md-5{padding-bottom:3rem !important}.pl-md-5,.px-md-5{padding-left:3rem !important}.m-md-n1{margin:-.25rem !important}.mt-md-n1,.my-md-n1{margin-top:-.25rem !important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem !important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem !important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem !important}.m-md-n2{margin:-.5rem !important}.mt-md-n2,.my-md-n2{margin-top:-.5rem !important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem !important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem !important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem !important}.m-md-n3{margin:-1rem !important}.mt-md-n3,.my-md-n3{margin-top:-1rem !important}.mr-md-n3,.mx-md-n3{margin-right:-1rem !important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem !important}.ml-md-n3,.mx-md-n3{margin-left:-1rem !important}.m-md-n4{margin:-1.5rem !important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem !important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem !important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem !important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem !important}.m-md-n5{margin:-3rem !important}.mt-md-n5,.my-md-n5{margin-top:-3rem !important}.mr-md-n5,.mx-md-n5{margin-right:-3rem !important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem !important}.ml-md-n5,.mx-md-n5{margin-left:-3rem !important}.m-md-auto{margin:auto !important}.mt-md-auto,.my-md-auto{margin-top:auto !important}.mr-md-auto,.mx-md-auto{margin-right:auto !important}.mb-md-auto,.my-md-auto{margin-bottom:auto !important}.ml-md-auto,.mx-md-auto{margin-left:auto !important}}@media (min-width: 992px){.m-lg-0{margin:0 !important}.mt-lg-0,.my-lg-0{margin-top:0 !important}.mr-lg-0,.mx-lg-0{margin-right:0 !important}.mb-lg-0,.my-lg-0{margin-bottom:0 !important}.ml-lg-0,.mx-lg-0{margin-left:0 !important}.m-lg-1{margin:.25rem !important}.mt-lg-1,.my-lg-1{margin-top:.25rem !important}.mr-lg-1,.mx-lg-1{margin-right:.25rem !important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem !important}.ml-lg-1,.mx-lg-1{margin-left:.25rem !important}.m-lg-2{margin:.5rem !important}.mt-lg-2,.my-lg-2{margin-top:.5rem !important}.mr-lg-2,.mx-lg-2{margin-right:.5rem !important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem !important}.ml-lg-2,.mx-lg-2{margin-left:.5rem !important}.m-lg-3{margin:1rem !important}.mt-lg-3,.my-lg-3{margin-top:1rem !important}.mr-lg-3,.mx-lg-3{margin-right:1rem !important}.mb-lg-3,.my-lg-3{margin-bottom:1rem !important}.ml-lg-3,.mx-lg-3{margin-left:1rem !important}.m-lg-4{margin:1.5rem !important}.mt-lg-4,.my-lg-4{margin-top:1.5rem !important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem !important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem !important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem !important}.m-lg-5{margin:3rem !important}.mt-lg-5,.my-lg-5{margin-top:3rem !important}.mr-lg-5,.mx-lg-5{margin-right:3rem !important}.mb-lg-5,.my-lg-5{margin-bottom:3rem !important}.ml-lg-5,.mx-lg-5{margin-left:3rem !important}.p-lg-0{padding:0 !important}.pt-lg-0,.py-lg-0{padding-top:0 !important}.pr-lg-0,.px-lg-0{padding-right:0 !important}.pb-lg-0,.py-lg-0{padding-bottom:0 !important}.pl-lg-0,.px-lg-0{padding-left:0 !important}.p-lg-1{padding:.25rem !important}.pt-lg-1,.py-lg-1{padding-top:.25rem !important}.pr-lg-1,.px-lg-1{padding-right:.25rem !important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem !important}.pl-lg-1,.px-lg-1{padding-left:.25rem !important}.p-lg-2{padding:.5rem !important}.pt-lg-2,.py-lg-2{padding-top:.5rem !important}.pr-lg-2,.px-lg-2{padding-right:.5rem !important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem !important}.pl-lg-2,.px-lg-2{padding-left:.5rem !important}.p-lg-3{padding:1rem !important}.pt-lg-3,.py-lg-3{padding-top:1rem !important}.pr-lg-3,.px-lg-3{padding-right:1rem !important}.pb-lg-3,.py-lg-3{padding-bottom:1rem !important}.pl-lg-3,.px-lg-3{padding-left:1rem !important}.p-lg-4{padding:1.5rem !important}.pt-lg-4,.py-lg-4{padding-top:1.5rem !important}.pr-lg-4,.px-lg-4{padding-right:1.5rem !important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem !important}.pl-lg-4,.px-lg-4{padding-left:1.5rem !important}.p-lg-5{padding:3rem !important}.pt-lg-5,.py-lg-5{padding-top:3rem !important}.pr-lg-5,.px-lg-5{padding-right:3rem !important}.pb-lg-5,.py-lg-5{padding-bottom:3rem !important}.pl-lg-5,.px-lg-5{padding-left:3rem !important}.m-lg-n1{margin:-.25rem !important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem !important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem !important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem !important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem !important}.m-lg-n2{margin:-.5rem !important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem !important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem !important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem !important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem !important}.m-lg-n3{margin:-1rem !important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem !important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem !important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem !important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem !important}.m-lg-n4{margin:-1.5rem !important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem !important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem !important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem !important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem !important}.m-lg-n5{margin:-3rem !important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem !important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem !important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem !important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem !important}.m-lg-auto{margin:auto !important}.mt-lg-auto,.my-lg-auto{margin-top:auto !important}.mr-lg-auto,.mx-lg-auto{margin-right:auto !important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto !important}.ml-lg-auto,.mx-lg-auto{margin-left:auto !important}}@media (min-width: 1200px){.m-xl-0{margin:0 !important}.mt-xl-0,.my-xl-0{margin-top:0 !important}.mr-xl-0,.mx-xl-0{margin-right:0 !important}.mb-xl-0,.my-xl-0{margin-bottom:0 !important}.ml-xl-0,.mx-xl-0{margin-left:0 !important}.m-xl-1{margin:.25rem !important}.mt-xl-1,.my-xl-1{margin-top:.25rem !important}.mr-xl-1,.mx-xl-1{margin-right:.25rem !important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem !important}.ml-xl-1,.mx-xl-1{margin-left:.25rem !important}.m-xl-2{margin:.5rem !important}.mt-xl-2,.my-xl-2{margin-top:.5rem !important}.mr-xl-2,.mx-xl-2{margin-right:.5rem !important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem !important}.ml-xl-2,.mx-xl-2{margin-left:.5rem !important}.m-xl-3{margin:1rem !important}.mt-xl-3,.my-xl-3{margin-top:1rem !important}.mr-xl-3,.mx-xl-3{margin-right:1rem !important}.mb-xl-3,.my-xl-3{margin-bottom:1rem !important}.ml-xl-3,.mx-xl-3{margin-left:1rem !important}.m-xl-4{margin:1.5rem !important}.mt-xl-4,.my-xl-4{margin-top:1.5rem !important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem !important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem !important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem !important}.m-xl-5{margin:3rem !important}.mt-xl-5,.my-xl-5{margin-top:3rem !important}.mr-xl-5,.mx-xl-5{margin-right:3rem !important}.mb-xl-5,.my-xl-5{margin-bottom:3rem !important}.ml-xl-5,.mx-xl-5{margin-left:3rem !important}.p-xl-0{padding:0 !important}.pt-xl-0,.py-xl-0{padding-top:0 !important}.pr-xl-0,.px-xl-0{padding-right:0 !important}.pb-xl-0,.py-xl-0{padding-bottom:0 !important}.pl-xl-0,.px-xl-0{padding-left:0 !important}.p-xl-1{padding:.25rem !important}.pt-xl-1,.py-xl-1{padding-top:.25rem !important}.pr-xl-1,.px-xl-1{padding-right:.25rem !important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem !important}.pl-xl-1,.px-xl-1{padding-left:.25rem !important}.p-xl-2{padding:.5rem !important}.pt-xl-2,.py-xl-2{padding-top:.5rem !important}.pr-xl-2,.px-xl-2{padding-right:.5rem !important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem !important}.pl-xl-2,.px-xl-2{padding-left:.5rem !important}.p-xl-3{padding:1rem !important}.pt-xl-3,.py-xl-3{padding-top:1rem !important}.pr-xl-3,.px-xl-3{padding-right:1rem !important}.pb-xl-3,.py-xl-3{padding-bottom:1rem !important}.pl-xl-3,.px-xl-3{padding-left:1rem !important}.p-xl-4{padding:1.5rem !important}.pt-xl-4,.py-xl-4{padding-top:1.5rem !important}.pr-xl-4,.px-xl-4{padding-right:1.5rem !important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem !important}.pl-xl-4,.px-xl-4{padding-left:1.5rem !important}.p-xl-5{padding:3rem !important}.pt-xl-5,.py-xl-5{padding-top:3rem !important}.pr-xl-5,.px-xl-5{padding-right:3rem !important}.pb-xl-5,.py-xl-5{padding-bottom:3rem !important}.pl-xl-5,.px-xl-5{padding-left:3rem !important}.m-xl-n1{margin:-.25rem !important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem !important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem !important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem !important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem !important}.m-xl-n2{margin:-.5rem !important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem !important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem !important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem !important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem !important}.m-xl-n3{margin:-1rem !important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem !important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem !important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem !important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem !important}.m-xl-n4{margin:-1.5rem !important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem !important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem !important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem !important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem !important}.m-xl-n5{margin:-3rem !important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem !important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem !important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem !important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem !important}.m-xl-auto{margin:auto !important}.mt-xl-auto,.my-xl-auto{margin-top:auto !important}.mr-xl-auto,.mx-xl-auto{margin-right:auto !important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto !important}.ml-xl-auto,.mx-xl-auto{margin-left:auto !important}}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace !important}.text-justify{text-align:justify !important}.text-wrap{white-space:normal !important}.text-nowrap,.card-admin-table th,.card-admin-table .badges-list{white-space:nowrap !important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left !important}.text-right,.card-admin-table .badges-list{text-align:right !important}.text-center,.control-image-preview,.card-admin-stat,.card-admin-analytics-summary,.card-admin-table .row-select label,.card-admin-table .blankslate td{text-align:center !important}@media (min-width: 576px){.text-sm-left{text-align:left !important}.text-sm-right{text-align:right !important}.text-sm-center{text-align:center !important}}@media (min-width: 768px){.text-md-left{text-align:left !important}.text-md-right{text-align:right !important}.text-md-center{text-align:center !important}}@media (min-width: 992px){.text-lg-left{text-align:left !important}.text-lg-right{text-align:right !important}.text-lg-center{text-align:center !important}}@media (min-width: 1200px){.text-xl-left{text-align:left !important}.text-xl-right{text-align:right !important}.text-xl-center{text-align:center !important}}.text-lowercase{text-transform:lowercase !important}.text-uppercase{text-transform:uppercase !important}.text-capitalize{text-transform:capitalize !important}.font-weight-light{font-weight:300 !important}.font-weight-lighter{font-weight:lighter !important}.font-weight-normal{font-weight:400 !important}.font-weight-bold,.card-admin-table .item-name,.card-admin-table h5{font-weight:700 !important}.font-weight-bolder{font-weight:bolder !important}.font-italic{font-style:italic !important}.text-white{color:#fff !important}.text-primary{color:#6554c0 !important}a.text-primary:hover,a.text-primary:focus{color:#443692 !important}.text-secondary{color:#a5adba !important}a.text-secondary:hover,a.text-secondary:focus{color:#7a8699 !important}.text-success{color:#36b37e !important}a.text-success:hover,a.text-success:focus{color:#247855 !important}.text-info{color:#00b8d9 !important}a.text-info:hover,a.text-info:focus{color:#00778d !important}.text-warning{color:#ffc107 !important}a.text-warning:hover,a.text-warning:focus{color:#ba8b00 !important}.text-danger{color:#ff5630 !important}a.text-danger:hover,a.text-danger:focus{color:#e32a00 !important}.text-light{color:#ebecf0 !important}a.text-light:hover,a.text-light:focus{color:#bfc2cf !important}.text-dark{color:#505f79 !important}a.text-dark:hover,a.text-dark:focus{color:#323b4b !important}.text-body{color:#172b4d !important}.text-muted,.control-time-picker span,.control-image-preview div,.page-header h1 small{color:#a5adba !important}.text-black-50{color:rgba(9,30,66,0.5) !important}.text-white-50{color:rgba(255,255,255,0.5) !important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none !important}.text-break{word-break:break-word !important;overflow-wrap:break-word !important}.text-reset{color:inherit !important}.visible{visibility:visible !important}.invisible{visibility:hidden !important}html,body{height:100%}.control-checkboxselect .form-check,.control-radioselect .form-check{padding:0}.control-checkboxselect .form-check-label,.control-radioselect .form-check-label{font-size:.875rem}.control-month-picker{width:350px}.control-time-picker{width:280px;height:100%;font-size:4rem}.control-time-picker .row{height:100%}.control-time-picker input{font-size:4rem}.control-time-picker span{position:relative;bottom:.25rem}.control-image-preview{background-color:#fff}.control-image-preview img{max-width:300px;max-height:200px}.control-image-preview div{font-size:.875rem}.login-form{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.login-form-container{width:100%;padding:30px}.login-form-logo{margin-bottom:2rem;text-align:center}.login-form-logo img{max-width:200px;max-height:64px}.login-form-card{max-width:340px;margin:0 auto}.login-form-title{font-size:1.25rem;text-align:center}.navbar-brand{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.navbar-brand img{height:2rem;margin-right:.5rem;border-radius:.25rem}.navbar .btn-user{overflow:hidden}.navbar .btn-user img{height:2rem}.col-nav-side{max-width:240px}.nav-side .media-icon{width:30px;text-align:center}.nav-side .nav-link.active{color:#091e42}.nav-side .nav-link.active .media-icon{color:#6554c0}.nav-side .nav-section.active{font-weight:700}.nav-side .nav-action .media-body{margin-left:30px}.page-header h1{font-size:1.25rem}.page-header h1 a{color:#172b4d}.page-header h1 small{font-size:1.25rem;vertical-align:middle}.card-admin-settings-card{line-height:1.5;color:#172b4d}.card-admin-settings-card:hover,.card-admin-settings-card:focus{text-decoration:none;background-color:#ebecf0}.card-admin-settings-card h5{font-size:1rem;line-height:1.5}.card-admin-info .card-title{font-size:1.25rem}.card-admin-info .card-body,.card-admin-info .card-admin-form .form-fieldset,.card-admin-form .card-admin-info .form-fieldset{padding:.75rem}.card-admin-stat{font-size:1.25rem}.media-admin-check{font-size:.875rem}.media-admin-check h5{font-size:1rem}.media-check-icon{width:1.5rem;height:1.5rem;font-size:1rem;line-height:1rem;color:#fff;background:#6b778c}.media-check-icon.media-check-icon-warning{background:#ffab00}.media-check-icon.media-check-icon-danger{background:#ff5630}.media-check-icon.media-check-icon-success{background:#36b37e}.media-check-icon .spinner-border{width:1rem;height:1rem}.card-admin-analytics-summary{width:200px}.card-admin-analytics-summary div{font-size:2rem}.card-admin-analytics-summary small{font-size:1rem}.card-admin-analytics-chart{margin-top:-10px}.card-admin-table .card-title{font-size:1.25rem}.card-admin-table th,.card-admin-table td{vertical-align:middle}.card-admin-table th{font-size:.875rem;color:#6b778c;background-color:rgba(0,0,0,0)}.card-admin-table .row-select input,.card-admin-table th input{font-size:1.25rem}.card-admin-table .card-body,.card-admin-table .card-admin-form .form-fieldset,.card-admin-form .card-admin-table .form-fieldset{padding:.75rem}.card-admin-table table+.card-body,.card-admin-table .card-admin-form table+.form-fieldset,.card-admin-form .card-admin-table table+.form-fieldset{border-top:1px solid #dfe1e6}.card-admin-table .btn-thumbnail{width:2rem;height:2rem;padding:0;background-color:transparent;background-size:cover}.card-admin-table .item-name{color:#172b4d}.card-admin-table h5{font-size:.875rem;line-height:1.5}.card-admin-table h5 a{color:#172b4d}.card-admin-table .badges-list{width:0%}.card-admin-table .row-select{width:1px}.card-admin-table [data-timestamp]{text-decoration:none}.card-admin-form .card-header h5{font-size:1.25rem}.card-admin-form .form-fieldset{margin:0 -1.25rem}.login-error{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.login-error-container{width:100%;padding:30px}.login-error-card{max-width:540px;margin:0 auto}.login-error-title{font-size:1.25rem;text-align:center}
 

BIN
misago/static/misago/img/attachment-403.png


BIN
misago/static/misago/img/attachment-404.png


BIN
misago/static/misago/img/blank-avatar.png


BIN
misago/static/misago/img/error-403.png


BIN
misago/static/misago/img/error-404.png


+ 9 - 0
misago/templates/misago/admin/conf/captcha_settings.html

@@ -30,4 +30,13 @@
 
   </fieldset>
 </div>
+<div class="form-fieldset">
+  <fieldset>
+    <legend>{% trans "Stop Forum Spam" %}</legend>
+
+    {% form_row form.enable_stop_forum_spam %}
+    {% form_row form.stop_forum_spam_confidence %}
+
+  </fieldset>
+</div>
 {% endblock form-body %}

+ 47 - 4
misago/templates/misago/admin/conf/threads_settings.html

@@ -5,19 +5,62 @@
 {% block form-body %}
 <div class="form-fieldset">
   <fieldset>
-    <legend>{% trans "Threads" %}</legend>
+    <legend>{% trans "Posting" %}</legend>
 
     {% form_row form.thread_title_length_min %}
     {% form_row form.thread_title_length_max %}
     
+    {% form_row form.post_length_min %}
+    {% form_row form.post_length_max %}
+
+    {% form_row form.post_attachments_limit %}
+    {% form_row form.unused_attachments_lifetime %}
+
+    {% form_row form.hourly_post_limit %}
+    {% form_row form.daily_post_limit %}
+    
   </fieldset>
 </div>
 <div class="form-fieldset">
   <fieldset>
-    <legend>{% trans "Posts" %}</legend>
+    <legend>{% trans "Threads lists" %}</legend>
+    
+    {% form_row form.threads_per_page %}
 
-    {% form_row form.post_length_min %}
-    {% form_row form.post_length_max %}
+  </fieldset>
+</div>
+<div class="form-fieldset">
+  <fieldset>
+    <legend>{% trans "Thread pages" %}</legend>
+    
+    {% form_row form.posts_per_page %}
+    {% form_row form.posts_per_page_orphans %}
+    {% form_row form.events_per_page %}
+
+  </fieldset>
+</div>
+<div class="form-fieldset">
+  <fieldset>
+    <legend>{% trans "Read-tracker" %}</legend>
+    
+    {% form_row form.readtracker_cutoff %}
+
+  </fieldset>
+</div>
+<div class="form-fieldset">
+  <fieldset>
+    <legend>{% trans "Attachment error images" %}</legend>
+
+    {% with form.attachment_403_image_delete as delete_field %}
+      {% with form_settings.attachment_403_image as setting %}
+        {% form_image_row form.attachment_403_image delete_field=delete_field size=setting.image_size dimensions=setting.image_dimensions %}
+      {% endwith %}
+    {% endwith %}
+    {% with form.attachment_404_image_delete as delete_field %}
+      {% with form_settings.attachment_404_image as setting %}
+        {% form_image_row form.attachment_404_image delete_field=delete_field size=setting.image_size dimensions=setting.image_dimensions %}
+      {% endwith %}
+    {% endwith %}
 
   </fieldset>
 </div>

+ 53 - 1
misago/templates/misago/admin/conf/users_settings.html

@@ -5,11 +5,20 @@
 {% block form-body %}
 <div class="form-fieldset">
   <fieldset>
-    <legend>{% trans "Basic settings" %}</legend>
+    <legend>{% trans "New accounts" %}</legend>
 
     {% form_row form.account_activation %}
+    {% form_row form.new_inactive_accounts_delete %}
+
+  </fieldset>
+</div>
+<div class="form-fieldset">
+  <fieldset>
+    <legend>{% trans "Usernames" %}</legend>
+
     {% form_row form.username_length_min %}
     {% form_row form.username_length_max %}
+    {% form_row form.anonymous_username %}
 
   </fieldset>
 </div>
@@ -39,6 +48,31 @@
 </div>
 <div class="form-fieldset">
   <fieldset>
+    <legend>{% trans "Data downloads" %}</legend>
+
+    {% form_row form.allow_data_downloads %}
+    {% form_row form.data_downloads_expiration %}
+
+  </fieldset>
+</div>
+<div class="form-fieldset">
+  <fieldset>
+    <legend>{% trans "Own account deletion" %}</legend>
+
+    {% form_row form.allow_delete_own_account %}
+
+  </fieldset>
+</div>
+<div class="form-fieldset">
+  <fieldset>
+    <legend>{% trans "IP addresses" %}</legend>
+
+    {% form_row form.ip_storage_time %}
+
+  </fieldset>
+</div>
+<div class="form-fieldset">
+  <fieldset>
     <legend>{% trans "Default subscription preferences" %}</legend>
 
     {% form_row form.subscribe_start %}
@@ -46,4 +80,22 @@
 
   </fieldset>
 </div>
+<div class="form-fieldset">
+  <fieldset>
+    <legend>{% trans "Users lists" %}</legend>
+
+    {% form_row form.users_per_page %}
+    {% form_row form.users_per_page_orphans %}
+
+  </fieldset>
+</div>
+<div class="form-fieldset">
+  <fieldset>
+    <legend>{% trans "Top posters ranking" %}</legend>
+
+    {% form_row form.top_posters_ranking_size %}
+    {% form_row form.top_posters_ranking_length %}
+
+  </fieldset>
+</div>
 {% endblock form-body %}

+ 2 - 2
misago/templates/misago/admin/dashboard/checks.html

@@ -46,7 +46,7 @@
             </div>
             <div class="media-body">
               <h5>{% trans "Forum address is not configured." %}</h5>
-              {% trans "Misago uses this setting to build links in e-mails sent to site users." %}
+              {% trans "Links in e-mails sent by Misago will be broken." %}
             </div>
           </div>
         </div>
@@ -79,7 +79,7 @@
                 {% endblocktrans %}
               </div>
               <div>
-                {% trans "Misago uses this setting to build links in e-mails sent to site users." %}
+                {% trans "Links in e-mails sentby Misagoill be broken." %}
               </div>
             </div>
           </div>

+ 7 - 7
misago/templates/misago/navbar.html

@@ -2,13 +2,13 @@
 <nav class="navbar navbar-misago navbar-default navbar-static-top" role="navigation">
 
   <div class="container navbar-full navbar-desktop-nav">
-    {% if settings.forum_branding_text or settings.forum_branding_logo %}
+    {% if settings.logo_text or settings.logo %}
       <a href="{% url 'misago:index' %}" class="navbar-brand">
-        {% if settings.forum_branding_logo %}
-          <img src="{{ settings.forum_branding_logo }}" alt="">
+        {% if settings.logo %}
+          <img src="{{ settings.logo }}" alt="">
         {% endif %}
-        {% if settings.forum_branding_text %}
-          <span>{{ settings.forum_branding_text}}</span>
+        {% if settings.logo_text %}
+          <span>{{ settings.logo_text}}</span>
         {% endif %}
       </a>
     {% endif %}
@@ -48,10 +48,10 @@
   </div><!-- /full navbar -->
 
   <ul class="nav navbar-nav navbar-compact-nav" itemscope itemtype="http://schema.org/SiteNavigationElement">
-    {% if settings.forum_branding_icon %}
+    {% if settings.logo_small %}
       <li>
         <a href="{% url 'misago:index' %}" class="brand-link">
-          <img src="{{ forum_branding_icon }}" alt="">
+          <img src="{{ settings.logo_small }}" alt="">
         </a>
       </li>
       {% if THREADS_ON_INDEX %}

+ 4 - 1
misago/threads/admin/__init__.py

@@ -42,5 +42,8 @@ class MisagoAdminExtension:
         )
 
         site.add_node(
-            name=_("Attachment types"), parent="settings", namespace="attachment-types"
+            name=_("Attachment types"),
+            description=_("Specify what files may be uploaded on the forum."),
+            parent="settings",
+            namespace="attachment-types",
         )

+ 5 - 4
misago/threads/api/postendpoints/delete.py

@@ -1,6 +1,5 @@
 from rest_framework.response import Response
 
-from ....conf import settings
 from ...moderation import posts as moderation
 from ...permissions import (
     allow_delete_best_answer,
@@ -9,8 +8,6 @@ from ...permissions import (
 )
 from ...serializers import DeletePostsSerializer
 
-DELETE_LIMIT = settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL
-
 
 def delete_post(request, thread, post):
     if post.is_event:
@@ -28,7 +25,11 @@ def delete_post(request, thread, post):
 def delete_bulk(request, thread):
     serializer = DeletePostsSerializer(
         data={"posts": request.data},
-        context={"thread": thread, "user_acl": request.user_acl},
+        context={
+            "settings": request.settings,
+            "thread": thread,
+            "user_acl": request.user_acl,
+        },
     )
 
     if not serializer.is_valid():

+ 6 - 1
misago/threads/api/postendpoints/merge.py

@@ -11,7 +11,12 @@ def posts_merge_endpoint(request, thread):
         raise PermissionDenied(_("You can't merge posts in this thread."))
 
     serializer = MergePostsSerializer(
-        data=request.data, context={"thread": thread, "user_acl": request.user_acl}
+        data=request.data,
+        context={
+            "settings": request.settings,
+            "thread": thread,
+            "user_acl": request.user_acl,
+        },
     )
 
     if not serializer.is_valid():

+ 6 - 1
misago/threads/api/postendpoints/move.py

@@ -11,7 +11,12 @@ def posts_move_endpoint(request, thread, viewmodel):
 
     serializer = MovePostsSerializer(
         data=request.data,
-        context={"request": request, "thread": thread, "viewmodel": viewmodel},
+        context={
+            "request": request,
+            "settings": request.settings,
+            "thread": thread,
+            "viewmodel": viewmodel,
+        },
     )
 
     if not serializer.is_valid():

+ 17 - 7
misago/threads/api/postendpoints/patch_post.py

@@ -1,5 +1,5 @@
 from django.core.exceptions import PermissionDenied
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext as _, ngettext
 from rest_framework import serializers
 from rest_framework.response import Response
 
@@ -17,8 +17,6 @@ from ...permissions import (
     exclude_invisible_posts,
 )
 
-PATCH_LIMIT = settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL
-
 post_patch_dispatcher = ApiPatch()
 
 
@@ -140,7 +138,9 @@ def post_patch_endpoint(request, post):
 
 
 def bulk_patch_endpoint(request, thread):
-    serializer = BulkPatchSerializer(data=request.data)
+    serializer = BulkPatchSerializer(
+        data=request.data, context={"settings": request.settings}
+    )
     if not serializer.is_valid():
         return Response(serializer.errors, status=400)
 
@@ -184,10 +184,20 @@ def clean_posts_for_patch(request, thread, posts_ids):
 
 class BulkPatchSerializer(serializers.Serializer):
     ids = serializers.ListField(
-        child=serializers.IntegerField(min_value=1),
-        max_length=PATCH_LIMIT,
-        min_length=1,
+        child=serializers.IntegerField(min_value=1), min_length=1
     )
     ops = serializers.ListField(
         child=serializers.DictField(), min_length=1, max_length=10
     )
+
+    def validate_ids(self, data):
+        settings = self.context["settings"]
+        limit = settings.posts_per_page + settings.posts_per_page_orphans
+        if len(data) > limit:
+            message = ngettext(
+                "No more than %(limit)s post can be updated at a single time.",
+                "No more than %(limit)s posts can be updated at a single time.",
+                limit,
+            )
+            raise serializers.ValidationError(message % {"limit": limit})
+        return data

+ 2 - 2
misago/threads/api/postendpoints/read.py

@@ -5,14 +5,14 @@ from ....readtracker.signals import thread_read
 
 
 def post_read_endpoint(request, thread, post):
-    poststracker.make_read_aware(request.user, post)
+    poststracker.make_read_aware(request, post)
     if post.is_new:
         poststracker.save_read(request.user, post)
         if thread.subscription and thread.subscription.last_read_on < post.posted_on:
             thread.subscription.last_read_on = post.posted_on
             thread.subscription.save()
 
-    threadstracker.make_read_aware(request.user, request.user_acl, thread)
+    threadstracker.make_read_aware(request, thread)
 
     # send signal if post read marked thread as read
     # used in some places, eg. syncing unread thread count

+ 17 - 17
misago/threads/api/postingendpoint/attachments.py

@@ -5,7 +5,6 @@ from rest_framework.fields import empty
 
 from . import PostingEndpoint, PostingMiddleware
 from ....acl.objectacl import add_acl_to_obj
-from ....conf import settings
 from ...serializers import AttachmentSerializer
 
 
@@ -21,6 +20,7 @@ class AttachmentsMiddleware(PostingMiddleware):
                 "user": self.user,
                 "user_acl": self.user_acl,
                 "post": self.post,
+                "settings": self.settings,
             },
         )
 
@@ -42,12 +42,10 @@ class AttachmentsSerializer(serializers.Serializer):
     def validate_attachments(self, ids):
         ids = list(set(ids))
 
-        validate_attachments_count(ids)
+        validate_attachments_count(ids, self.context["settings"])
 
-        attachments = self.get_initial_attachments(
-            self.context["mode"], self.context["user_acl"], self.context["post"]
-        )
-        new_attachments = self.get_new_attachments(self.context["user"], ids)
+        attachments = self.get_initial_attachments()
+        new_attachments = self.get_new_attachments(ids)
 
         if not attachments and not new_attachments:
             return []  # no attachments
@@ -74,20 +72,22 @@ class AttachmentsSerializer(serializers.Serializer):
             self.final_attachments += new_attachments
             self.final_attachments.sort(key=lambda a: a.pk, reverse=True)
 
-    def get_initial_attachments(self, mode, user_acl, post):
+    def get_initial_attachments(self):
         attachments = []
-        if mode == PostingEndpoint.EDIT:
-            queryset = post.attachment_set.select_related("filetype")
+        if self.context["mode"] == PostingEndpoint.EDIT:
+            queryset = self.context["post"].attachment_set.select_related("filetype")
             attachments = list(queryset)
-            add_acl_to_obj(user_acl, attachments)
+            add_acl_to_obj(self.context["user_acl"], attachments)
         return attachments
 
-    def get_new_attachments(self, user, ids):
+    def get_new_attachments(self, ids):
         if not ids:
             return []
 
-        queryset = user.attachment_set.select_related("filetype").filter(
-            post__isnull=True, id__in=ids
+        queryset = (
+            self.context["user"]
+            .attachment_set.select_related("filetype")
+            .filter(post__isnull=True, id__in=ids)
         )
 
         return list(queryset)
@@ -124,19 +124,19 @@ class AttachmentsSerializer(serializers.Serializer):
         post.update_fields.append("attachments_cache")
 
 
-def validate_attachments_count(data):
+def validate_attachments_count(data, settings):
     total_attachments = len(data)
-    if total_attachments > settings.MISAGO_POST_ATTACHMENTS_LIMIT:
+    if total_attachments > settings.post_attachments_limit:
         # pylint: disable=line-too-long
         message = ngettext(
             "You can't attach more than %(limit_value)s file to single post (added %(show_value)s).",
             "You can't attach more than %(limit_value)s flies to single post (added %(show_value)s).",
-            settings.MISAGO_POST_ATTACHMENTS_LIMIT,
+            settings.post_attachments_limit,
         )
         raise serializers.ValidationError(
             message
             % {
-                "limit_value": settings.MISAGO_POST_ATTACHMENTS_LIMIT,
+                "limit_value": settings.post_attachments_limit,
                 "show_value": total_attachments,
             }
         )

+ 10 - 9
misago/threads/api/postingendpoint/floodprotection.py

@@ -4,9 +4,8 @@ from django.utils import timezone
 from django.utils.translation import gettext as _
 
 from . import PostingEndpoint, PostingInterrupt, PostingMiddleware
-from ....conf import settings
 
-MIN_POSTING_PAUSE = 3
+MIN_POSTING_INTERVAL = 3
 
 
 class FloodProtectionMiddleware(PostingMiddleware):
@@ -21,7 +20,7 @@ class FloodProtectionMiddleware(PostingMiddleware):
 
         if self.user.last_posted_on:
             previous_post = now - self.user.last_posted_on
-            if previous_post.total_seconds() < MIN_POSTING_PAUSE:
+            if previous_post.total_seconds() < MIN_POSTING_INTERVAL:
                 raise PostingInterrupt(
                     _("You can't post message so quickly after previous one.")
                 )
@@ -29,15 +28,17 @@ class FloodProtectionMiddleware(PostingMiddleware):
         self.user.last_posted_on = timezone.now()
         self.user.update_fields.append("last_posted_on")
 
-        if settings.MISAGO_HOURLY_POST_LIMIT:
+        if self.settings.hourly_post_limit:
             cutoff = now - timedelta(hours=24)
-            if self.is_limit_exceeded(cutoff, settings.MISAGO_HOURLY_POST_LIMIT):
-                raise PostingInterrupt(_("Your account has excceed hourly post limit."))
+            if self.is_limit_exceeded(cutoff, self.settings.hourly_post_limit):
+                raise PostingInterrupt(
+                    _("Your account has exceed an hourly post limit.")
+                )
 
-        if settings.MISAGO_DIALY_POST_LIMIT:
+        if self.settings.daily_post_limit:
             cutoff = now - timedelta(hours=1)
-            if self.is_limit_exceeded(cutoff, settings.MISAGO_DIALY_POST_LIMIT):
-                raise PostingInterrupt(_("Your account has excceed dialy post limit."))
+            if self.is_limit_exceeded(cutoff, self.settings.daily_post_limit):
+                raise PostingInterrupt(_("Your account has exceed a daily post limit."))
 
     def is_limit_exceeded(self, cutoff, limit):
         return self.user.post_set.filter(posted_on__gte=cutoff).count() >= limit

+ 5 - 1
misago/threads/api/threadendpoints/delete.py

@@ -16,7 +16,11 @@ def delete_thread(request, thread):
 def delete_bulk(request, viewmodel):
     serializer = DeleteThreadsSerializer(
         data={"threads": request.data},
-        context={"request": request, "viewmodel": viewmodel},
+        context={
+            "request": request,
+            "settings": request.settings,
+            "viewmodel": viewmodel,
+        },
     )
 
     if not serializer.is_valid():

+ 16 - 7
misago/threads/api/threadendpoints/patch.py

@@ -2,7 +2,7 @@ from django.contrib.auth import get_user_model
 from django.core.exceptions import PermissionDenied, ValidationError
 from django.http import Http404
 from django.shortcuts import get_object_or_404
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext as _, ngettext
 from rest_framework import serializers
 from rest_framework.response import Response
 
@@ -42,8 +42,6 @@ from ...permissions import (
 from ...serializers import ThreadParticipantSerializer
 from ...validators import validate_thread_title
 
-PATCH_LIMIT = settings.MISAGO_THREADS_PER_PAGE
-
 User = get_user_model()
 
 thread_patch_dispatcher = ApiPatch()
@@ -411,7 +409,9 @@ def thread_patch_endpoint(request, thread):
 def bulk_patch_endpoint(
     request, viewmodel
 ):  # pylint: disable=too-many-branches, too-many-locals
-    serializer = BulkPatchSerializer(data=request.data)
+    serializer = BulkPatchSerializer(
+        data=request.data, context={"settings": request.settings}
+    )
     if not serializer.is_valid():
         return Response(serializer.errors, status=400)
 
@@ -483,10 +483,19 @@ def clean_threads_for_patch(request, viewmodel, threads_ids):
 
 class BulkPatchSerializer(serializers.Serializer):
     ids = serializers.ListField(
-        child=serializers.IntegerField(min_value=1),
-        max_length=PATCH_LIMIT,
-        min_length=1,
+        child=serializers.IntegerField(min_value=1), min_length=1
     )
     ops = serializers.ListField(
         child=serializers.DictField(), min_length=1, max_length=10
     )
+
+    def validate_ids(self, data):
+        limit = self.context["settings"].threads_per_page
+        if len(data) > limit:
+            message = ngettext(
+                "No more than %(limit)s thread can be updated at a single time.",
+                "No more than %(limit)s threads can be updated at a single time.",
+                limit,
+            )
+            raise ValidationError(message % {"limit": limit})
+        return data

+ 5 - 5
misago/threads/management/commands/clearattachments.py

@@ -4,7 +4,7 @@ from datetime import timedelta
 from django.core.management.base import BaseCommand
 from django.utils import timezone
 
-from ....conf import settings
+from ....conf.shortcuts import get_dynamic_settings
 from ....core.management.progressbar import show_progress
 from ....core.pgutils import chunk_queryset
 from ...models import Attachment
@@ -14,15 +14,15 @@ class Command(BaseCommand):
     help = "Deletes attachments unassociated with any posts"
 
     def handle(self, *args, **options):
-        cutoff = timezone.now() - timedelta(
-            minutes=settings.MISAGO_ATTACHMENT_ORPHANED_EXPIRE
-        )
+        settings = get_dynamic_settings()
+
+        cutoff = timezone.now() - timedelta(hours=settings.unused_attachments_lifetime)
         queryset = Attachment.objects.filter(post__isnull=True, uploaded_on__lt=cutoff)
 
         attachments_to_sync = queryset.count()
 
         if not attachments_to_sync:
-            self.stdout.write("\n\nNo attachments were found")
+            self.stdout.write("\n\nNo unused attachments were cleared")
         else:
             self.sync_attachments(queryset, attachments_to_sync)
 

+ 6 - 6
misago/threads/search.py

@@ -11,8 +11,6 @@ from .serializers import FeedSerializer
 from .utils import add_categories_to_items
 from .viewmodels import ThreadsRootCategory
 
-HITS_CEILING = settings.MISAGO_POSTS_PER_PAGE * 5
-
 
 class SearchThreads(SearchProvider):
     name = _("Threads")
@@ -34,8 +32,8 @@ class SearchThreads(SearchProvider):
         list_page = paginate(
             results,
             page,
-            settings.MISAGO_POSTS_PER_PAGE,
-            settings.MISAGO_POSTS_TAIL,
+            self.request.settings.posts_per_page,
+            self.request.settings.posts_per_page_orphans,
             allow_explicit_first_page=True,
         )
         paginator = pagination_dict(list_page)
@@ -66,6 +64,8 @@ class SearchThreads(SearchProvider):
 
 
 def search_threads(request, query, visible_threads):
+    max_hits = request.settings.posts_per_page * 5
+
     search_query = SearchQuery(
         filter_search(query), config=settings.MISAGO_SEARCH_CONFIG
     )
@@ -81,8 +81,8 @@ def search_threads(request, query, visible_threads):
         search_vector=search_query,
     )
 
-    if queryset[: HITS_CEILING + 1].count() > HITS_CEILING:
-        queryset = queryset.order_by("-id")[:HITS_CEILING]
+    if queryset[: max_hits + 1].count() > max_hits:
+        queryset = queryset.order_by("-id")[:max_hits]
 
     return (
         Post.objects.filter(id__in=queryset.values("id"))

+ 40 - 33
misago/threads/serializers/moderation.py

@@ -27,9 +27,6 @@ from ..threadtypes import trees_map
 from ..utils import get_thread_id_from_url
 from ..validators import validate_category, validate_thread_title
 
-POSTS_LIMIT = settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL
-THREADS_LIMIT = settings.MISAGO_THREADS_PER_PAGE
-
 
 __all__ = [
     "DeletePostsSerializer",
@@ -43,6 +40,10 @@ __all__ = [
 ]
 
 
+def get_posts_limit(settings):
+    return settings.posts_per_page + settings.posts_per_page_orphans
+
+
 class DeletePostsSerializer(serializers.Serializer):
     error_empty_or_required = gettext_lazy(
         "You have to specify at least one post to delete."
@@ -63,13 +64,14 @@ class DeletePostsSerializer(serializers.Serializer):
     )
 
     def validate_posts(self, data):
-        if len(data) > POSTS_LIMIT:
+        limit = get_posts_limit(self.context["settings"])
+        if len(data) > limit:
             message = ngettext(
-                "No more than %(limit)s post can be deleted at single time.",
-                "No more than %(limit)s posts can be deleted at single time.",
-                POSTS_LIMIT,
+                "No more than %(limit)s post can be deleted at a single time.",
+                "No more than %(limit)s posts can be deleted at a single time.",
+                limit,
             )
-            raise ValidationError(message % {"limit": POSTS_LIMIT})
+            raise ValidationError(message % {"limit": limit})
 
         user_acl = self.context["user_acl"]
         thread = self.context["thread"]
@@ -116,17 +118,18 @@ class MergePostsSerializer(serializers.Serializer):
     )
 
     def validate_posts(self, data):
+        limit = get_posts_limit(self.context["settings"])
         data = list(set(data))
 
         if len(data) < 2:
             raise serializers.ValidationError(self.error_empty_or_required)
-        if len(data) > POSTS_LIMIT:
+        if len(data) > limit:
             message = ngettext(
-                "No more than %(limit)s post can be merged at single time.",
-                "No more than %(limit)s posts can be merged at single time.",
-                POSTS_LIMIT,
+                "No more than %(limit)s post can be merged at a single time.",
+                "No more than %(limit)s posts can be merged at a single time.",
+                limit,
             )
-            raise serializers.ValidationError(message % {"limit": POSTS_LIMIT})
+            raise serializers.ValidationError(message % {"limit": limit})
 
         user_acl = self.context["user_acl"]
         thread = self.context["thread"]
@@ -240,14 +243,15 @@ class MovePostsSerializer(serializers.Serializer):
         return new_thread
 
     def validate_posts(self, data):
+        limit = get_posts_limit(self.context["settings"])
         data = list(set(data))
-        if len(data) > POSTS_LIMIT:
+        if len(data) > limit:
             message = ngettext(
-                "No more than %(limit)s post can be moved at single time.",
-                "No more than %(limit)s posts can be moved at single time.",
-                POSTS_LIMIT,
+                "No more than %(limit)s post can be moved at a single time.",
+                "No more than %(limit)s posts can be moved at a single time.",
+                limit,
             )
-            raise serializers.ValidationError(message % {"limit": POSTS_LIMIT})
+            raise serializers.ValidationError(message % {"limit": limit})
 
         request = self.context["request"]
         thread = self.context["thread"]
@@ -367,13 +371,14 @@ class SplitPostsSerializer(NewThreadSerializer):
     )
 
     def validate_posts(self, data):
-        if len(data) > POSTS_LIMIT:
+        limit = get_posts_limit(self.context["settings"])
+        if len(data) > limit:
             message = ngettext(
-                "No more than %(limit)s post can be split at single time.",
-                "No more than %(limit)s posts can be split at single time.",
-                POSTS_LIMIT,
+                "No more than %(limit)s post can be split at a single time.",
+                "No more than %(limit)s posts can be split at a single time.",
+                limit,
             )
-            raise ValidationError(message % {"limit": POSTS_LIMIT})
+            raise ValidationError(message % {"limit": limit})
 
         thread = self.context["thread"]
         user_acl = self.context["user_acl"]
@@ -421,13 +426,14 @@ class DeleteThreadsSerializer(serializers.Serializer):
     )
 
     def validate_threads(self, data):
-        if len(data) > THREADS_LIMIT:
+        limit = self.context["settings"].threads_per_page
+        if len(data) > limit:
             message = ngettext(
-                "No more than %(limit)s thread can be deleted at single time.",
-                "No more than %(limit)s threads can be deleted at single time.",
-                THREADS_LIMIT,
+                "No more than %(limit)s thread can be deleted at a single time.",
+                "No more than %(limit)s threads can be deleted at a single time.",
+                limit,
             )
-            raise ValidationError(message % {"limit": THREADS_LIMIT})
+            raise ValidationError(message % {"limit": limit})
 
         request = self.context["request"]
         viewmodel = self.context["viewmodel"]
@@ -543,13 +549,14 @@ class MergeThreadsSerializer(NewThreadSerializer):
     )
 
     def validate_threads(self, data):
-        if len(data) > THREADS_LIMIT:
+        limit = self.context["settings"].threads_per_page
+        if len(data) > limit:
             message = ngettext(
-                "No more than %(limit)s thread can be merged at single time.",
-                "No more than %(limit)s threads can be merged at single time.",
-                POSTS_LIMIT,
+                "No more than %(limit)s thread can be merged at a single time.",
+                "No more than %(limit)s threads can be merged at a single time.",
+                limit,
             )
-            raise ValidationError(message % {"limit": THREADS_LIMIT})
+            raise ValidationError(message % {"limit": limit})
 
         threads_tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
 

+ 8 - 8
misago/threads/tests/test_anonymize_data.py

@@ -45,7 +45,7 @@ class AnonymizeEventsTests(AuthenticatedUserTestCase):
         make_participants_aware(self.user, self.thread)
         change_owner(request, self.thread, user)
 
-        user.anonymize_data()
+        user.anonymize_data(anonymous_username="Deleted")
 
         event = Post.objects.get(event_type="changed_owner")
         self.assertEqual(
@@ -68,7 +68,7 @@ class AnonymizeEventsTests(AuthenticatedUserTestCase):
         make_participants_aware(self.user, self.thread)
         add_participant(request, self.thread, user)
 
-        user.anonymize_data()
+        user.anonymize_data(anonymous_username="Deleted")
 
         event = Post.objects.get(event_type="added_participant")
         self.assertEqual(
@@ -94,7 +94,7 @@ class AnonymizeEventsTests(AuthenticatedUserTestCase):
         make_participants_aware(user, self.thread)
         remove_participant(request, self.thread, user)
 
-        user.anonymize_data()
+        user.anonymize_data(anonymous_username="Deleted")
 
         event = Post.objects.get(event_type="owner_left")
         self.assertEqual(
@@ -120,7 +120,7 @@ class AnonymizeEventsTests(AuthenticatedUserTestCase):
         make_participants_aware(user, self.thread)
         remove_participant(request, self.thread, user)
 
-        user.anonymize_data()
+        user.anonymize_data(anonymous_username="Deleted")
 
         event = Post.objects.get(event_type="removed_owner")
         self.assertEqual(
@@ -146,7 +146,7 @@ class AnonymizeEventsTests(AuthenticatedUserTestCase):
         make_participants_aware(user, self.thread)
         remove_participant(request, self.thread, user)
 
-        user.anonymize_data()
+        user.anonymize_data(anonymous_username="Deleted")
 
         event = Post.objects.get(event_type="participant_left")
         self.assertEqual(
@@ -172,7 +172,7 @@ class AnonymizeEventsTests(AuthenticatedUserTestCase):
         make_participants_aware(self.user, self.thread)
         remove_participant(request, self.thread, user)
 
-        user.anonymize_data()
+        user.anonymize_data(anonymous_username="Deleted")
 
         event = Post.objects.get(event_type="removed_participant")
         self.assertEqual(
@@ -211,7 +211,7 @@ class AnonymizeLikesTests(AuthenticatedUserTestCase):
         patch_is_liked(self.get_request(self.user), post, 1)
         patch_is_liked(self.get_request(user), post, 1)
 
-        user.anonymize_data()
+        user.anonymize_data(anonymous_username="Deleted")
 
         last_likes = Post.objects.get(pk=post.pk).last_likes
         self.assertEqual(
@@ -242,7 +242,7 @@ class AnonymizePostsTests(AuthenticatedUserTestCase):
 
         user = create_test_user("OtherUser", "otheruser@example.com")
         post = test.reply_thread(thread, poster=user)
-        user.anonymize_data()
+        user.anonymize_data(anonymous_username="Deleted")
 
         anonymized_post = Post.objects.get(pk=post.pk)
         self.assertTrue(anonymized_post.is_valid)

+ 290 - 298
misago/threads/tests/test_attachments_middleware.py

@@ -1,14 +1,10 @@
 from unittest.mock import Mock
 
+import pytest
 from rest_framework import serializers
 
 from .. import test
-from ...acl import useracl
-from ...acl.test import patch_user_acl
-from ...categories.models import Category
-from ...conf import settings
-from ...conftest import get_cache_versions
-from ...users.test import AuthenticatedUserTestCase
+from ...conf.test import override_dynamic_settings
 from ..api.postingendpoint import PostingEndpoint
 from ..api.postingendpoint.attachments import (
     AttachmentsMiddleware,
@@ -16,295 +12,291 @@ from ..api.postingendpoint.attachments import (
 )
 from ..models import Attachment, AttachmentType
 
-cache_versions = get_cache_versions()
-
-
-def patch_attachments_acl(acl_patch=None):
-    acl_patch = acl_patch or {}
-    acl_patch.setdefault("max_attachment_size", 1024)
-    return patch_user_acl(acl_patch)
-
-
-class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
-    def setUp(self):
-        super().setUp()
-
-        self.category = Category.objects.get(slug="first-category")
-        self.thread = test.post_thread(category=self.category)
-        self.post = self.thread.first_post
-
-        self.post.update_fields = []
-
-        self.filetype = AttachmentType.objects.order_by("id").last()
-
-    def mock_attachment(self, user=True, post=None):
-        return Attachment.objects.create(
-            secret=Attachment.generate_new_secret(),
-            filetype=self.filetype,
-            post=post,
-            size=1000,
-            uploader=self.user if user else None,
-            uploader_name=self.user.username,
-            uploader_slug=self.user.slug,
-            filename="testfile_%s.zip" % (Attachment.objects.count() + 1),
-        )
-
-    def test_use_this_middleware(self):
-        """use_this_middleware returns False if we can't upload attachments"""
-        with patch_user_acl({"max_attachment_size": 0}):
-            user_acl = useracl.get_user_acl(self.user, cache_versions)
-            middleware = AttachmentsMiddleware(user=self.user, user_acl=user_acl)
-            self.assertFalse(middleware.use_this_middleware())
-
-        with patch_user_acl({"max_attachment_size": 1024}):
-            user_acl = useracl.get_user_acl(self.user, cache_versions)
-            middleware = AttachmentsMiddleware(user=self.user, user_acl=user_acl)
-            self.assertTrue(middleware.use_this_middleware())
-
-    @patch_attachments_acl()
-    def test_middleware_is_optional(self):
-        """middleware is optional"""
-        INPUTS = [{}, {"attachments": []}]
-
-        user_acl = useracl.get_user_acl(self.user, cache_versions)
-
-        for test_input in INPUTS:
-            middleware = AttachmentsMiddleware(
-                request=Mock(data=test_input),
-                mode=PostingEndpoint.START,
-                user=self.user,
-                user_acl=user_acl,
-                post=self.post,
-            )
-
-            serializer = middleware.get_serializer()
-            self.assertTrue(serializer.is_valid())
-
-    @patch_attachments_acl()
-    def test_middleware_validates_ids(self):
-        """middleware validates attachments ids"""
-        INPUTS = [
-            "none",
-            ["a", "b", 123],
-            range(settings.MISAGO_POST_ATTACHMENTS_LIMIT + 1),
-        ]
-
-        user_acl = useracl.get_user_acl(self.user, cache_versions)
-
-        for test_input in INPUTS:
-            middleware = AttachmentsMiddleware(
-                request=Mock(data={"attachments": test_input}),
-                mode=PostingEndpoint.START,
-                user=self.user,
-                user_acl=user_acl,
-                post=self.post,
-            )
-
-            serializer = middleware.get_serializer()
-            self.assertFalse(
-                serializer.is_valid(), "%r shouldn't validate" % test_input
-            )
-
-    @patch_attachments_acl()
-    def test_get_initial_attachments(self):
-        """
-        get_initial_attachments returns list of attachments already existing on post
-        """
-        user_acl = useracl.get_user_acl(self.user, cache_versions)
-        middleware = AttachmentsMiddleware(
-            request=Mock(data={}),
-            mode=PostingEndpoint.EDIT,
-            user=self.user,
-            user_acl=user_acl,
-            post=self.post,
-        )
-
-        serializer = middleware.get_serializer()
-
-        attachments = serializer.get_initial_attachments(
-            middleware.mode, middleware.user, middleware.post
-        )
-        self.assertEqual(attachments, [])
-
-        attachment = self.mock_attachment(post=self.post)
-        attachments = serializer.get_initial_attachments(
-            middleware.mode, middleware.user_acl, middleware.post
-        )
-        self.assertEqual(attachments, [attachment])
-
-    @patch_attachments_acl()
-    def test_get_new_attachments(self):
-        """
-        get_initial_attachments returns list of attachments already existing on post
-        """
-        user_acl = useracl.get_user_acl(self.user, cache_versions)
-        middleware = AttachmentsMiddleware(
-            request=Mock(data={}),
-            mode=PostingEndpoint.EDIT,
-            user=self.user,
-            user_acl=user_acl,
-            post=self.post,
-        )
-
-        serializer = middleware.get_serializer()
-
-        attachments = serializer.get_new_attachments(middleware.user, [1, 2, 3])
-        self.assertEqual(attachments, [])
-
-        attachment = self.mock_attachment()
-        attachments = serializer.get_new_attachments(middleware.user, [attachment.pk])
-        self.assertEqual(attachments, [attachment])
-
-        # only own orphaned attachments may be assigned to posts
-        other_user_attachment = self.mock_attachment(user=False)
-        attachments = serializer.get_new_attachments(
-            middleware.user, [other_user_attachment.pk]
-        )
-        self.assertEqual(attachments, [])
-
-    @patch_attachments_acl({"can_delete_other_users_attachments": False})
-    def test_cant_delete_attachment(self):
-        """
-        middleware validates if we have permission to delete other users attachments
-        """
-        attachment = self.mock_attachment(user=False, post=self.post)
-        self.assertIsNone(attachment.uploader)
-
-        user_acl = useracl.get_user_acl(self.user, cache_versions)
-        serializer = AttachmentsMiddleware(
-            request=Mock(data={"attachments": []}),
-            mode=PostingEndpoint.EDIT,
-            user=self.user,
-            user_acl=user_acl,
-            post=self.post,
-        ).get_serializer()
-
-        self.assertFalse(serializer.is_valid())
-
-    @patch_attachments_acl()
-    def test_add_attachments(self):
-        """middleware adds attachments to post"""
-        attachments = [self.mock_attachment(), self.mock_attachment()]
-
-        user_acl = useracl.get_user_acl(self.user, cache_versions)
-        middleware = AttachmentsMiddleware(
-            request=Mock(data={"attachments": [a.pk for a in attachments]}),
-            mode=PostingEndpoint.EDIT,
-            user=self.user,
-            user_acl=user_acl,
-            post=self.post,
-        )
-
-        serializer = middleware.get_serializer()
-        self.assertTrue(serializer.is_valid())
-        middleware.save(serializer)
-
-        # attachments were associated with post
-        self.assertEqual(self.post.update_fields, ["attachments_cache"])
-        self.assertEqual(self.post.attachment_set.count(), 2)
-
-        attachments_filenames = list(reversed([a.filename for a in attachments]))
-        self.assertEqual(
-            [a["filename"] for a in self.post.attachments_cache], attachments_filenames
-        )
-
-    @patch_attachments_acl()
-    def test_remove_attachments(self):
-        """middleware removes attachment from post and db"""
-        attachments = [
-            self.mock_attachment(post=self.post),
-            self.mock_attachment(post=self.post),
-        ]
-
-        user_acl = useracl.get_user_acl(self.user, cache_versions)
-        middleware = AttachmentsMiddleware(
-            request=Mock(data={"attachments": [attachments[0].pk]}),
-            mode=PostingEndpoint.EDIT,
-            user=self.user,
-            user_acl=user_acl,
-            post=self.post,
-        )
-
-        serializer = middleware.get_serializer()
-        self.assertTrue(serializer.is_valid())
-        middleware.save(serializer)
-
-        # attachments were associated with post
-        self.assertEqual(self.post.update_fields, ["attachments_cache"])
-        self.assertEqual(self.post.attachment_set.count(), 1)
-
-        self.assertEqual(Attachment.objects.count(), 1)
-
-        attachments_filenames = [attachments[0].filename]
-        self.assertEqual(
-            [a["filename"] for a in self.post.attachments_cache], attachments_filenames
-        )
-
-    @patch_attachments_acl()
-    def test_steal_attachments(self):
-        """middleware validates if attachments are already assigned to other posts"""
-        other_post = test.reply_thread(self.thread)
-
-        attachments = [self.mock_attachment(post=other_post), self.mock_attachment()]
-
-        user_acl = useracl.get_user_acl(self.user, cache_versions)
-        middleware = AttachmentsMiddleware(
-            request=Mock(data={"attachments": [attachments[0].pk, attachments[1].pk]}),
-            mode=PostingEndpoint.EDIT,
-            user=self.user,
-            user_acl=user_acl,
-            post=self.post,
-        )
-
-        serializer = middleware.get_serializer()
-        self.assertTrue(serializer.is_valid())
-        middleware.save(serializer)
-
-        # only unassociated attachment was associated with post
-        self.assertEqual(self.post.update_fields, ["attachments_cache"])
-        self.assertEqual(self.post.attachment_set.count(), 1)
-
-        self.assertEqual(Attachment.objects.get(pk=attachments[0].pk).post, other_post)
-        self.assertEqual(Attachment.objects.get(pk=attachments[1].pk).post, self.post)
-
-    @patch_attachments_acl()
-    def test_edit_attachments(self):
-        """middleware removes and adds attachments to post"""
-        attachments = [
-            self.mock_attachment(post=self.post),
-            self.mock_attachment(post=self.post),
-            self.mock_attachment(),
-        ]
-
-        user_acl = useracl.get_user_acl(self.user, cache_versions)
-        middleware = AttachmentsMiddleware(
-            request=Mock(data={"attachments": [attachments[0].pk, attachments[2].pk]}),
-            mode=PostingEndpoint.EDIT,
-            user=self.user,
-            user_acl=user_acl,
-            post=self.post,
-        )
-
-        serializer = middleware.get_serializer()
-        self.assertTrue(serializer.is_valid())
-        middleware.save(serializer)
-
-        # attachments were associated with post
-        self.assertEqual(self.post.update_fields, ["attachments_cache"])
-        self.assertEqual(self.post.attachment_set.count(), 2)
-
-        attachments_filenames = [attachments[2].filename, attachments[0].filename]
-        self.assertEqual(
-            [a["filename"] for a in self.post.attachments_cache], attachments_filenames
-        )
-
-
-class ValidateAttachmentsCountTests(AuthenticatedUserTestCase):
-    def test_validate_attachments_count(self):
-        """too large count of attachments is rejected"""
-        validate_attachments_count(range(settings.MISAGO_POST_ATTACHMENTS_LIMIT))
-
-        with self.assertRaises(serializers.ValidationError):
-            validate_attachments_count(
-                range(settings.MISAGO_POST_ATTACHMENTS_LIMIT + 1)
-            )
+
+@pytest.fixture
+def context(default_category, dynamic_settings, user, user_acl):
+    thread = test.post_thread(category=default_category)
+    post = thread.first_post
+    post.update_fields = []
+
+    return {
+        "category": default_category,
+        "thread": thread,
+        "post": post,
+        "settings": dynamic_settings,
+        "user": user,
+        "user_acl": user_acl,
+    }
+
+
+def create_attachment(*, post=None, user=None):
+    return Attachment.objects.create(
+        secret=Attachment.generate_new_secret(),
+        filetype=AttachmentType.objects.order_by("id").last(),
+        post=post,
+        size=1000,
+        uploader=user if user else None,
+        uploader_name=user.username if user else "testuser",
+        uploader_slug=user.slug if user else "testuser",
+        filename="testfile_%s.zip" % (Attachment.objects.count() + 1),
+    )
+
+
+def test_middleware_is_used_if_user_has_permission_to_upload_attachments(context):
+    context["user_acl"]["max_attachment_size"] = 1024
+    middleware = AttachmentsMiddleware(**context)
+    assert middleware.use_this_middleware()
+
+
+def test_middleware_is_not_used_if_user_has_no_permission_to_upload_attachments(
+    context
+):
+    context["user_acl"]["max_attachment_size"] = 0
+    middleware = AttachmentsMiddleware(**context)
+    assert not middleware.use_this_middleware()
+
+
+def test_middleware_handles_no_data(context):
+    middleware = AttachmentsMiddleware(
+        request=Mock(data={}), mode=PostingEndpoint.START, **context
+    )
+
+    serializer = middleware.get_serializer()
+    assert serializer.is_valid()
+
+
+def test_middleware_handles_empty_data(context):
+    middleware = AttachmentsMiddleware(
+        request=Mock(data={"attachments": []}), mode=PostingEndpoint.START, **context
+    )
+
+    serializer = middleware.get_serializer()
+    assert serializer.is_valid()
+
+
+def test_data_validation_fails_if_attachments_data_is_not_iterable(context):
+    middleware = AttachmentsMiddleware(
+        request=Mock(data={"attachments": "none"}),
+        mode=PostingEndpoint.START,
+        **context
+    )
+
+    serializer = middleware.get_serializer()
+    assert not serializer.is_valid()
+
+
+def test_data_validation_fails_if_attachments_data_has_non_int_values(context):
+    middleware = AttachmentsMiddleware(
+        request=Mock(data={"attachments": [1, "b"]}),
+        mode=PostingEndpoint.START,
+        **context
+    )
+
+    serializer = middleware.get_serializer()
+    assert not serializer.is_valid()
+
+
+@override_dynamic_settings(post_attachments_limit=2)
+def test_data_validation_fails_if_attachments_data_is_longer_than_allowed(context):
+    middleware = AttachmentsMiddleware(
+        request=Mock(data={"attachments": range(5)}),
+        mode=PostingEndpoint.START,
+        **context
+    )
+
+    serializer = middleware.get_serializer()
+    assert not serializer.is_valid()
+
+
+def test_middleware_adds_attachment_to_new_post(context):
+    new_attachment = create_attachment(user=context["user"])
+    middleware = AttachmentsMiddleware(
+        request=Mock(data={"attachments": [new_attachment.id]}),
+        mode=PostingEndpoint.START,
+        **context
+    )
+    serializer = middleware.get_serializer()
+    serializer.is_valid()
+    middleware.save(serializer)
+
+    new_attachment.refresh_from_db()
+    assert new_attachment.post == context["post"]
+
+
+def test_middleware_adds_attachment_to_attachments_cache(context):
+    new_attachment = create_attachment(user=context["user"])
+    middleware = AttachmentsMiddleware(
+        request=Mock(data={"attachments": [new_attachment.id]}),
+        mode=PostingEndpoint.START,
+        **context
+    )
+    serializer = middleware.get_serializer()
+    serializer.is_valid()
+    middleware.save(serializer)
+
+    attachments_cache = context["post"].attachments_cache
+    assert len(attachments_cache) == 1
+    assert attachments_cache[0]["id"] == new_attachment.id
+
+
+def test_middleware_adds_attachment_to_existing_post(context):
+    new_attachment = create_attachment(user=context["user"])
+    middleware = AttachmentsMiddleware(
+        request=Mock(data={"attachments": [new_attachment.id]}),
+        mode=PostingEndpoint.EDIT,
+        **context
+    )
+    serializer = middleware.get_serializer()
+    serializer.is_valid()
+    middleware.save(serializer)
+
+    new_attachment.refresh_from_db()
+    assert new_attachment.post == context["post"]
+
+
+def test_middleware_adds_attachment_to_post_with_existing_attachment(context):
+    old_attachment = create_attachment(post=context["post"])
+    new_attachment = create_attachment(user=context["user"])
+    middleware = AttachmentsMiddleware(
+        request=Mock(data={"attachments": [old_attachment.id, new_attachment.id]}),
+        mode=PostingEndpoint.EDIT,
+        **context
+    )
+    serializer = middleware.get_serializer()
+    serializer.is_valid()
+    middleware.save(serializer)
+
+    new_attachment.refresh_from_db()
+    assert new_attachment.post == context["post"]
+
+    old_attachment.refresh_from_db()
+    assert old_attachment.post == context["post"]
+
+
+def test_middleware_adds_attachment_to_existing_attachments_cache(context):
+    old_attachment = create_attachment(post=context["post"])
+    new_attachment = create_attachment(user=context["user"])
+    middleware = AttachmentsMiddleware(
+        request=Mock(data={"attachments": [old_attachment.id, new_attachment.id]}),
+        mode=PostingEndpoint.EDIT,
+        **context
+    )
+    serializer = middleware.get_serializer()
+    serializer.is_valid()
+    middleware.save(serializer)
+
+    attachments_cache = context["post"].attachments_cache
+    assert len(attachments_cache) == 2
+    assert attachments_cache[0]["id"] == new_attachment.id
+    assert attachments_cache[1]["id"] == old_attachment.id
+
+
+def test_other_user_attachment_cant_be_added_to_post(context):
+    attachment = create_attachment()
+    middleware = AttachmentsMiddleware(
+        request=Mock(data={"attachments": [attachment.id]}),
+        mode=PostingEndpoint.EDIT,
+        **context
+    )
+    serializer = middleware.get_serializer()
+    serializer.is_valid()
+    middleware.save(serializer)
+
+    attachment.refresh_from_db()
+    assert not attachment.post
+
+
+def test_other_post_attachment_cant_be_added_to_new_post(context, default_category):
+    post = test.post_thread(category=default_category).first_post
+    attachment = create_attachment(post=post, user=context["user"])
+    middleware = AttachmentsMiddleware(
+        request=Mock(data={"attachments": [attachment.id]}),
+        mode=PostingEndpoint.EDIT,
+        **context
+    )
+    serializer = middleware.get_serializer()
+    serializer.is_valid()
+    middleware.save(serializer)
+
+    attachment.refresh_from_db()
+    assert attachment.post == post
+
+
+def test_middleware_removes_attachment_from_post(context):
+    attachment = create_attachment(post=context["post"], user=context["user"])
+    middleware = AttachmentsMiddleware(
+        request=Mock(data={"attachments": []}), mode=PostingEndpoint.EDIT, **context
+    )
+    serializer = middleware.get_serializer()
+    serializer.is_valid()
+    middleware.save(serializer)
+
+    context["post"].refresh_from_db()
+    assert not context["post"].attachment_set.exists()
+
+
+def test_middleware_removes_attachment_from_attachments_cache(context):
+    attachment = create_attachment(post=context["post"], user=context["user"])
+    middleware = AttachmentsMiddleware(
+        request=Mock(data={"attachments": []}), mode=PostingEndpoint.EDIT, **context
+    )
+    serializer = middleware.get_serializer()
+    serializer.is_valid()
+    middleware.save(serializer)
+
+    assert not context["post"].attachments_cache
+
+
+def test_middleware_deletes_attachment_removed_from_post(context):
+    attachment = create_attachment(post=context["post"], user=context["user"])
+    middleware = AttachmentsMiddleware(
+        request=Mock(data={"attachments": []}), mode=PostingEndpoint.EDIT, **context
+    )
+    serializer = middleware.get_serializer()
+    serializer.is_valid()
+    middleware.save(serializer)
+
+    with pytest.raises(Attachment.DoesNotExist):
+        attachment.refresh_from_db()
+
+
+def test_middleware_blocks_user_from_removing_other_user_attachment_without_permission(
+    context
+):
+    attachment = create_attachment(post=context["post"])
+    middleware = AttachmentsMiddleware(
+        request=Mock(data={"attachments": []}), mode=PostingEndpoint.EDIT, **context
+    )
+    serializer = middleware.get_serializer()
+    assert not serializer.is_valid()
+    middleware.save(serializer)
+
+    attachment.refresh_from_db()
+    assert attachment.post == context["post"]
+
+
+def test_middleware_allows_user_with_permission_to_remove_other_user_attachment(
+    context
+):
+    context["user_acl"]["can_delete_other_users_attachments"] = True
+    attachment = create_attachment(post=context["post"])
+    middleware = AttachmentsMiddleware(
+        request=Mock(data={"attachments": []}), mode=PostingEndpoint.EDIT, **context
+    )
+    serializer = middleware.get_serializer()
+    serializer.is_valid()
+    middleware.save(serializer)
+
+    context["post"].refresh_from_db()
+    assert not context["post"].attachment_set.exists()
+
+
+def test_attachments_count_validator_allows_attachments_within_limit():
+    settings = Mock(post_attachments_limit=5)
+    validate_attachments_count(range(5), settings)
+
+
+def test_attachments_count_validator_raises_validation_error_on_too_many_attachmes():
+    settings = Mock(post_attachments_limit=2)
+    with pytest.raises(serializers.ValidationError):
+        validate_attachments_count(range(5), settings)

+ 273 - 0
misago/threads/tests/test_attachments_proxy.py

@@ -0,0 +1,273 @@
+import pytest
+from django.urls import reverse
+
+from ...acl.models import Role
+from ...acl.test import patch_user_acl
+from ...conf import settings
+from ...conf.test import override_dynamic_settings
+from ..models import Attachment, AttachmentType
+from ..test import post_thread
+
+
+@pytest.fixture
+def attachment_type(db):
+    return AttachmentType.objects.order_by("id").first()
+
+
+@pytest.fixture
+def attachment(attachment_type, post, user):
+    return Attachment.objects.create(
+        secret="secret",
+        filetype=attachment_type,
+        post=post,
+        uploader=user,
+        uploader_name=user.username,
+        uploader_slug=user.slug,
+        filename="test.txt",
+        file="test.txt",
+        size=1000,
+    )
+
+
+@pytest.fixture
+def image(post, user):
+    return Attachment.objects.create(
+        secret="secret",
+        filetype=AttachmentType.objects.get(mimetypes="image/png"),
+        post=post,
+        uploader=user,
+        uploader_name=user.username,
+        uploader_slug=user.slug,
+        filename="test.png",
+        image="test.png",
+        size=1000,
+    )
+
+
+@pytest.fixture
+def image_with_thumbnail(post, user):
+    return Attachment.objects.create(
+        secret="secret",
+        filetype=AttachmentType.objects.get(mimetypes="image/png"),
+        post=post,
+        uploader=user,
+        uploader_name=user.username,
+        uploader_slug=user.slug,
+        filename="test.png",
+        image="test.png",
+        thumbnail="test-thumbnail.png",
+        size=1000,
+    )
+
+
+@pytest.fixture
+def other_users_attachment(attachment, other_user):
+    attachment.uploader = other_user
+    attachment.save()
+    return attachment
+
+
+@pytest.fixture
+def orphaned_attachment(attachment):
+    attachment.post = None
+    attachment.save()
+    return attachment
+
+
+@pytest.fixture
+def other_users_orphaned_attachment(other_users_attachment):
+    other_users_attachment.post = None
+    other_users_attachment.save()
+    return other_users_attachment
+
+
+def assert_403(response):
+    assert response.status_code == 302
+    assert response["location"].endswith(settings.MISAGO_ATTACHMENT_403_IMAGE)
+
+
+def assert_404(response):
+    assert response.status_code == 302
+    assert response["location"].endswith(settings.MISAGO_ATTACHMENT_404_IMAGE)
+
+
+def test_proxy_redirects_client_to_attachment_file(client, attachment):
+    response = client.get(attachment.get_absolute_url())
+    assert response.status_code == 302
+    assert response["location"].endswith("test.txt")
+
+
+def test_proxy_redirects_client_to_attachment_image(client, image):
+    response = client.get(image.get_absolute_url())
+    assert response.status_code == 302
+    assert response["location"].endswith("test.png")
+
+
+def test_proxy_redirects_client_to_attachment_thumbnail(client, image_with_thumbnail):
+    response = client.get(image_with_thumbnail.get_thumbnail_url())
+    assert response.status_code == 302
+    assert response["location"].endswith("test-thumbnail.png")
+
+
+def test_proxy_redirects_to_404_image_for_nonexistant_attachment(db, client):
+    response = client.get(
+        reverse("misago:attachment", kwargs={"pk": 1, "secret": "secret"})
+    )
+    assert_404(response)
+
+
+def test_proxy_redirects_to_404_image_for_url_with_invalid_attachment_secret(
+    client, attachment
+):
+    response = client.get(
+        reverse("misago:attachment", kwargs={"pk": attachment.id, "secret": "invalid"})
+    )
+    assert_404(response)
+
+
+@patch_user_acl({"can_download_other_users_attachments": False})
+def test_proxy_redirects_to_403_image_for_user_without_permission_to_see_attachment(
+    user_client, other_users_attachment
+):
+    response = user_client.get(other_users_attachment.get_absolute_url())
+    assert_403(response)
+
+
+def test_thumbnail_proxy_redirects_to_404_for_non_image_attachment(client, attachment):
+    response = client.get(
+        reverse(
+            "misago:attachment-thumbnail",
+            kwargs={"pk": attachment.pk, "secret": attachment.secret},
+        )
+    )
+    assert_404(response)
+
+
+def test_thumbnail_proxy_redirects_to_regular_image_for_image_without_thumbnail(
+    client, image
+):
+    response = client.get(
+        reverse(
+            "misago:attachment-thumbnail",
+            kwargs={"pk": image.pk, "secret": image.secret},
+        )
+    )
+    assert response.status_code == 302
+    assert response["location"].endswith("test.png")
+
+
+def test_thumbnail_proxy_redirects_to_thumbnail_image(client, image_with_thumbnail):
+    response = client.get(
+        reverse(
+            "misago:attachment-thumbnail",
+            kwargs={
+                "pk": image_with_thumbnail.pk,
+                "secret": image_with_thumbnail.secret,
+            },
+        )
+    )
+    assert response.status_code == 302
+    assert response["location"].endswith("test-thumbnail.png")
+
+
+def test_proxy_blocks_user_from_their_orphaned_attachment(
+    user_client, orphaned_attachment
+):
+    response = user_client.get(orphaned_attachment.get_absolute_url())
+    assert_404(response)
+
+
+def test_proxy_redirects_user_to_their_orphaned_attachment_if_link_has_shva_key(
+    user_client, orphaned_attachment
+):
+    response = user_client.get("%s?shva=1" % orphaned_attachment.get_absolute_url())
+    assert response.status_code == 302
+    assert response["location"].endswith("test.txt")
+
+
+def test_proxy_blocks_user_from_other_users_orphaned_attachment(
+    user_client, other_users_orphaned_attachment
+):
+    response = user_client.get(other_users_orphaned_attachment.get_absolute_url())
+    assert_404(response)
+
+
+def test_proxy_blocks_user_from_other_users_orphaned_attachment_if_link_has_shva_key(
+    user_client, other_users_orphaned_attachment
+):
+    response = user_client.get(
+        "%s?shva=1" % other_users_orphaned_attachment.get_absolute_url()
+    )
+    assert_404(response)
+
+
+def test_proxy_redirects_staff_to_other_users_orphaned_attachment(
+    staff_client, orphaned_attachment
+):
+    response = staff_client.get("%s?shva=1" % orphaned_attachment.get_absolute_url())
+    assert response.status_code == 302
+    assert response["location"].endswith("test.txt")
+
+
+def test_proxy_blocks_user_from_attachment_with_disabled_type(
+    user_client, attachment, attachment_type
+):
+    attachment_type.status = AttachmentType.DISABLED
+    attachment_type.save()
+
+    response = user_client.get(attachment.get_absolute_url())
+    assert_403(response)
+
+
+@pytest.fixture
+def role(db):
+    return Role.objects.create(name="Test")
+
+
+@pytest.fixture
+def limited_attachment_type(attachment_type, role):
+    attachment_type.limit_downloads_to.add(role)
+    return attachment_type
+
+
+def test_proxy_blocks_user_without_role_from_attachment_with_limited_type(
+    user_client, attachment, limited_attachment_type
+):
+    response = user_client.get(attachment.get_absolute_url())
+    assert_403(response)
+
+
+def test_proxy_allows_user_with_role_to_download_attachment_with_limited_type(
+    user, user_client, role, attachment, limited_attachment_type
+):
+    user.roles.add(role)
+    response = user_client.get(attachment.get_absolute_url())
+    assert response.status_code == 302
+    assert response["location"].endswith("test.txt")
+
+
+def test_proxy_allows_staff_user_without_role_to_download_attachment_with_limited_type(
+    staff_client, role, attachment, limited_attachment_type
+):
+    response = staff_client.get(attachment.get_absolute_url())
+    assert response.status_code == 302
+    assert response["location"].endswith("test.txt")
+
+
+@override_dynamic_settings(attachment_403_image="custom-403-image.png")
+@patch_user_acl({"can_download_other_users_attachments": False})
+def test_proxy_uses_custom_permission_denied_image_if_one_is_set(
+    user_client, other_users_attachment
+):
+    response = user_client.get(other_users_attachment.get_absolute_url())
+    assert response.status_code == 302
+    assert response["location"].endswith("custom-403-image.png")
+
+
+@override_dynamic_settings(attachment_404_image="custom-404-image.png")
+def test_proxy_uses_custom_not_found_image_if_one_is_set(db, client):
+    response = client.get(
+        reverse("misago:attachment", kwargs={"pk": 1, "secret": "secret"})
+    )
+    assert response.status_code == 302
+    assert response["location"].endswith("custom-404-image.png")

+ 0 - 238
misago/threads/tests/test_attachmentview.py

@@ -1,238 +0,0 @@
-import os
-
-from django.urls import reverse
-
-from .. import test
-from ...acl.models import Role
-from ...acl.test import patch_user_acl
-from ...categories.models import Category
-from ...conf import settings
-from ...users.test import AuthenticatedUserTestCase
-from ..models import Attachment, AttachmentType
-
-TESTFILES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "testfiles")
-TEST_DOCUMENT_PATH = os.path.join(TESTFILES_DIR, "document.pdf")
-TEST_SMALLJPG_PATH = os.path.join(TESTFILES_DIR, "small.jpg")
-
-
-def patch_attachments_acl(acl_patch=None):
-    acl_patch = acl_patch or {}
-    acl_patch.setdefault("max_attachment_size", 1024)
-    acl_patch.setdefault("can_download_other_users_attachments", True)
-    return patch_user_acl(acl_patch)
-
-
-class AttachmentViewTestCase(AuthenticatedUserTestCase):
-    def setUp(self):
-        super().setUp()
-
-        AttachmentType.objects.all().delete()
-
-        self.category = Category.objects.get(slug="first-category")
-        self.post = test.post_thread(category=self.category).first_post
-
-        self.api_link = reverse("misago:api:attachment-list")
-
-        self.attachment_type_jpg = AttachmentType.objects.create(
-            name="JPG", extensions="jpeg,jpg"
-        )
-        self.attachment_type_pdf = AttachmentType.objects.create(
-            name="PDF", extensions="pdf"
-        )
-
-    def upload_document(self, is_orphaned=False, by_other_user=False):
-        with open(TEST_DOCUMENT_PATH, "rb") as upload:
-            response = self.client.post(self.api_link, data={"upload": upload})
-        self.assertEqual(response.status_code, 200)
-
-        attachment = Attachment.objects.order_by("id").last()
-
-        if not is_orphaned:
-            attachment.post = self.post
-            attachment.save()
-        if by_other_user:
-            attachment.uploader = None
-            attachment.save()
-
-        return attachment
-
-    def upload_image(self):
-        with open(TEST_SMALLJPG_PATH, "rb") as upload:
-            response = self.client.post(self.api_link, data={"upload": upload})
-        self.assertEqual(response.status_code, 200)
-
-        return Attachment.objects.order_by("id").last()
-
-    @patch_attachments_acl()
-    def assertIs404(self, response):
-        self.assertEqual(response.status_code, 302)
-        self.assertTrue(response["location"].endswith(settings.MISAGO_404_IMAGE))
-
-    @patch_attachments_acl()
-    def assertIs403(self, response):
-        self.assertEqual(response.status_code, 302)
-        self.assertTrue(response["location"].endswith(settings.MISAGO_403_IMAGE))
-
-    @patch_attachments_acl()
-    def assertSuccess(self, response):
-        self.assertEqual(response.status_code, 302)
-        self.assertFalse(response["location"].endswith(settings.MISAGO_404_IMAGE))
-        self.assertFalse(response["location"].endswith(settings.MISAGO_403_IMAGE))
-
-    @patch_attachments_acl()
-    def test_nonexistant_file(self):
-        """user tries to retrieve nonexistant file"""
-        response = self.client.get(
-            reverse("misago:attachment", kwargs={"pk": 123, "secret": "qwertyuiop"})
-        )
-
-        self.assertIs404(response)
-
-    @patch_attachments_acl()
-    def test_invalid_secret(self):
-        """user tries to retrieve existing file using invalid secret"""
-        attachment = self.upload_document()
-
-        response = self.client.get(
-            reverse(
-                "misago:attachment",
-                kwargs={"pk": attachment.pk, "secret": "qwertyuiop"},
-            )
-        )
-
-        self.assertIs404(response)
-
-    @patch_attachments_acl({"can_download_other_users_attachments": False})
-    def test_other_user_file_no_permission(self):
-        """user tries to retrieve other user's file without perm"""
-        attachment = self.upload_document(by_other_user=True)
-
-        response = self.client.get(attachment.get_absolute_url())
-        self.assertIs403(response)
-
-    @patch_attachments_acl({"can_download_other_users_attachments": False})
-    def test_other_user_orphaned_file(self):
-        """user tries to retrieve other user's orphaned file"""
-        attachment = self.upload_document(is_orphaned=True, by_other_user=True)
-
-        response = self.client.get(attachment.get_absolute_url())
-        self.assertIs404(response)
-
-        response = self.client.get(attachment.get_absolute_url() + "?shva=1")
-        self.assertIs404(response)
-
-    @patch_attachments_acl()
-    def test_document_thumbnail(self):
-        """user tries to retrieve thumbnail from non-image attachment"""
-        attachment = self.upload_document()
-
-        response = self.client.get(
-            reverse(
-                "misago:attachment-thumbnail",
-                kwargs={"pk": attachment.pk, "secret": attachment.secret},
-            )
-        )
-        self.assertIs404(response)
-
-    @patch_attachments_acl()
-    def test_no_role(self):
-        """user tries to retrieve attachment without perm to its type"""
-        attachment = self.upload_document()
-
-        user_roles = (r.pk for r in self.user.get_roles())
-        self.attachment_type_pdf.limit_downloads_to.set(
-            Role.objects.exclude(id__in=user_roles)
-        )
-
-        response = self.client.get(attachment.get_absolute_url())
-        self.assertIs403(response)
-
-    @patch_attachments_acl()
-    def test_type_disabled(self):
-        """user tries to retrieve attachment the type disabled downloads"""
-        attachment = self.upload_document()
-
-        self.attachment_type_pdf.status = AttachmentType.DISABLED
-        self.attachment_type_pdf.save()
-
-        response = self.client.get(attachment.get_absolute_url())
-        self.assertIs403(response)
-
-    @patch_attachments_acl()
-    def test_locked_type(self):
-        """user retrieves own locked file"""
-        attachment = self.upload_document()
-
-        self.attachment_type_pdf.status = AttachmentType.LOCKED
-        self.attachment_type_pdf.save()
-
-        response = self.client.get(attachment.get_absolute_url())
-        self.assertSuccess(response)
-
-    @patch_attachments_acl()
-    def test_own_file(self):
-        """user retrieves own file"""
-        attachment = self.upload_document()
-
-        response = self.client.get(attachment.get_absolute_url())
-        self.assertSuccess(response)
-
-    @patch_attachments_acl()
-    def test_other_user_file(self):
-        """user retrieves other user's file with perm"""
-        attachment = self.upload_document(by_other_user=True)
-
-        response = self.client.get(attachment.get_absolute_url())
-        self.assertSuccess(response)
-
-    @patch_attachments_acl()
-    def test_other_user_orphaned_file_is_staff(self):
-        """user retrieves other user's orphaned file because he is staff"""
-        attachment = self.upload_document(is_orphaned=True, by_other_user=True)
-
-        self.user.is_staff = True
-        self.user.save()
-
-        response = self.client.get(attachment.get_absolute_url())
-        self.assertIs404(response)
-
-        response = self.client.get(attachment.get_absolute_url() + "?shva=1")
-        self.assertSuccess(response)
-
-    @patch_attachments_acl()
-    def test_orphaned_file_is_uploader(self):
-        """user retrieves orphaned file because he is its uploader"""
-        attachment = self.upload_document(is_orphaned=True)
-
-        response = self.client.get(attachment.get_absolute_url())
-        self.assertIs404(response)
-
-        response = self.client.get(attachment.get_absolute_url() + "?shva=1")
-        self.assertSuccess(response)
-
-    @patch_attachments_acl()
-    def test_has_role(self):
-        """user retrieves file he has roles to download"""
-        attachment = self.upload_document()
-
-        user_roles = self.user.get_roles()
-        self.attachment_type_pdf.limit_downloads_to.set(user_roles)
-
-        response = self.client.get(attachment.get_absolute_url() + "?shva=1")
-        self.assertSuccess(response)
-
-    @patch_attachments_acl()
-    def test_image(self):
-        """user retrieves """
-        attachment = self.upload_image()
-
-        response = self.client.get(attachment.get_absolute_url() + "?shva=1")
-        self.assertSuccess(response)
-
-    @patch_attachments_acl()
-    def test_image_thumb(self):
-        """user retrieves image's thumbnail"""
-        attachment = self.upload_image()
-
-        response = self.client.get(attachment.get_absolute_url() + "?shva=1")
-        self.assertSuccess(response)

+ 58 - 73
misago/threads/tests/test_clearattachments.py

@@ -1,82 +1,67 @@
 from datetime import timedelta
 from io import StringIO
 
-from django.core.management import call_command
-from django.test import TestCase
+import pytest
+from django.core import management
 from django.utils import timezone
 
-from .. import test
-from ...categories.models import Category
-from ...conf import settings
+from ...conf.test import override_dynamic_settings
 from ..management.commands import clearattachments
 from ..models import Attachment, AttachmentType
 
 
-class ClearAttachmentsTests(TestCase):
-    def test_no_attachments_sync(self):
-        """command works when there are no attachments"""
-        command = clearattachments.Command()
-
-        out = StringIO()
-        call_command(command, stdout=out)
-        command_output = out.getvalue().strip()
-
-        self.assertEqual(command_output, "No attachments were found")
-
-    def test_attachments_sync(self):
-        """command synchronizes attachments"""
-        filetype = AttachmentType.objects.order_by("id").last()
-
-        # create 5 expired orphaned attachments
-        cutoff = timezone.now() - timedelta(
-            minutes=settings.MISAGO_ATTACHMENT_ORPHANED_EXPIRE
-        )
-        cutoff -= timedelta(minutes=5)
-
-        for _ in range(5):
-            Attachment.objects.create(
-                secret=Attachment.generate_new_secret(),
-                filetype=filetype,
-                size=1000,
-                uploaded_on=cutoff,
-                uploader_name="User",
-                uploader_slug="user",
-                filename="testfile_%s.zip" % (Attachment.objects.count() + 1),
-            )
-
-        # create 5 expired non-orphaned attachments
-        category = Category.objects.get(slug="first-category")
-        post = test.post_thread(category).first_post
-
-        for _ in range(5):
-            Attachment.objects.create(
-                secret=Attachment.generate_new_secret(),
-                filetype=filetype,
-                size=1000,
-                uploaded_on=cutoff,
-                post=post,
-                uploader_name="User",
-                uploader_slug="user",
-                filename="testfile_%s.zip" % (Attachment.objects.count() + 1),
-            )
-
-        # create 5 fresh orphaned attachments
-        for _ in range(5):
-            Attachment.objects.create(
-                secret=Attachment.generate_new_secret(),
-                filetype=filetype,
-                size=1000,
-                uploader_name="User",
-                uploader_slug="user",
-                filename="testfile_%s.zip" % (Attachment.objects.count() + 1),
-            )
-
-        command = clearattachments.Command()
-
-        out = StringIO()
-        call_command(command, stdout=out)
-
-        command_output = out.getvalue().splitlines()[-1].strip()
-        self.assertEqual(command_output, "Cleared 5 attachments")
-
-        self.assertEqual(Attachment.objects.count(), 10)
+@pytest.fixture
+def attachment_type(db):
+    return AttachmentType.objects.order_by("id").last()
+
+
+def create_attachment(attachment_type, uploaded_on, post=None):
+    return Attachment.objects.create(
+        secret=Attachment.generate_new_secret(),
+        post=post,
+        filetype=attachment_type,
+        size=1000,
+        uploaded_on=uploaded_on,
+        uploader_name="User",
+        uploader_slug="user",
+        filename="testfile_%s.zip" % (Attachment.objects.count() + 1),
+    )
+
+
+def call_command():
+    command = clearattachments.Command()
+
+    out = StringIO()
+    management.call_command(command, stdout=out)
+    return out.getvalue().strip().splitlines()[-1].strip()
+
+
+def test_command_works_if_there_are_no_attachments(db):
+    command_output = call_command()
+    assert command_output == "No unused attachments were cleared"
+
+
+@override_dynamic_settings(unused_attachments_lifetime=2)
+def test_recent_attachment_is_not_cleared(attachment_type):
+    attachment = create_attachment(attachment_type, timezone.now())
+    command_output = call_command()
+    assert command_output == "No unused attachments were cleared"
+
+
+@override_dynamic_settings(unused_attachments_lifetime=2)
+def test_old_used_attachment_is_not_cleared(attachment_type, post):
+    uploaded_on = timezone.now() - timedelta(hours=3)
+    attachment = create_attachment(attachment_type, uploaded_on, post)
+    command_output = call_command()
+    assert command_output == "No unused attachments were cleared"
+
+
+@override_dynamic_settings(unused_attachments_lifetime=2)
+def test_old_unused_attachment_is_cleared(attachment_type):
+    uploaded_on = timezone.now() - timedelta(hours=3)
+    attachment = create_attachment(attachment_type, uploaded_on)
+    command_output = call_command()
+    assert command_output == "Cleared 1 attachments"
+
+    with pytest.raises(Attachment.DoesNotExist):
+        attachment.refresh_from_db()

+ 10 - 10
misago/threads/tests/test_emailnotification_middleware.py

@@ -128,6 +128,7 @@ class EmailNotificationTests(AuthenticatedUserTestCase):
 
         self.assertEqual(len(mail.outbox), 0)
 
+    @override_dynamic_settings(forum_address="http://test.com/")
     @patch_category_acl({"can_reply_threads": True})
     def test_other_notified(self):
         """email is sent to subscriber"""
@@ -138,11 +139,10 @@ class EmailNotificationTests(AuthenticatedUserTestCase):
             send_email=True,
         )
 
-        with override_dynamic_settings(forum_address="http://test.com/"):
-            response = self.client.post(
-                self.api_link, data={"post": "This is test response!"}
-            )
-            self.assertEqual(response.status_code, 200)
+        response = self.client.post(
+            self.api_link, data={"post": "This is test response!"}
+        )
+        self.assertEqual(response.status_code, 200)
 
         self.assertEqual(len(mail.outbox), 1)
         last_email = mail.outbox[-1]
@@ -159,6 +159,7 @@ class EmailNotificationTests(AuthenticatedUserTestCase):
         last_post = self.thread.post_set.order_by("id").last()
         self.assertIn(last_post.get_absolute_url(), message)
 
+    @override_dynamic_settings(forum_address="http://test.com/")
     @patch_category_acl({"can_reply_threads": True})
     def test_other_notified_after_reading(self):
         """email is sent to subscriber that had sub updated by read api"""
@@ -169,11 +170,10 @@ class EmailNotificationTests(AuthenticatedUserTestCase):
             send_email=True,
         )
 
-        with override_dynamic_settings(forum_address="http://test.com/"):
-            response = self.client.post(
-                self.api_link, data={"post": "This is test response!"}
-            )
-            self.assertEqual(response.status_code, 200)
+        response = self.client.post(
+            self.api_link, data={"post": "This is test response!"}
+        )
+        self.assertEqual(response.status_code, 200)
 
         self.assertEqual(len(mail.outbox), 1)
         last_email = mail.outbox[-1]

+ 185 - 29
misago/threads/tests/test_floodprotection_middleware.py

@@ -1,49 +1,205 @@
 from datetime import timedelta
 
+import pytest
 from django.utils import timezone
 
-from ...users.test import AuthenticatedUserTestCase
-from ..api.postingendpoint import PostingInterrupt
+from ...conf.test import override_dynamic_settings
+from ..api.postingendpoint import PostingEndpoint, PostingInterrupt
 from ..api.postingendpoint.floodprotection import FloodProtectionMiddleware
+from ..test import post_thread
 
-user_acl = {"can_omit_flood_protection": False}
+default_acl = {"can_omit_flood_protection": False}
+can_omit_flood_acl = {"can_omit_flood_protection": True}
 
 
-class FloodProtectionMiddlewareTests(AuthenticatedUserTestCase):
-    def test_flood_protection_middleware_on_no_posts(self):
-        """middleware sets last_posted_on on user"""
-        self.user.update_fields = []
-        self.assertIsNone(self.user.last_posted_on)
+def test_middleware_lets_users_first_post_through(dynamic_settings, user):
+    user.update_fields = []
+    middleware = FloodProtectionMiddleware(
+        settings=dynamic_settings, user=user, user_acl=default_acl
+    )
+    middleware.interrupt_posting(None)
 
-        middleware = FloodProtectionMiddleware(user=self.user, user_acl=user_acl)
+
+def test_middleware_updates_users_last_post_datetime(dynamic_settings, user):
+    user.update_fields = []
+    middleware = FloodProtectionMiddleware(
+        settings=dynamic_settings, user=user, user_acl=default_acl
+    )
+    middleware.interrupt_posting(None)
+    assert user.last_posted_on
+    assert "last_posted_on" in user.update_fields
+
+
+def test_middleware_interrupts_posting_because_previous_post_was_posted_too_recently(
+    dynamic_settings, user
+):
+    user.last_posted_on = timezone.now()
+    user.update_fields = []
+
+    middleware = FloodProtectionMiddleware(
+        mode=PostingEndpoint.START,
+        settings=dynamic_settings,
+        user=user,
+        user_acl=default_acl,
+    )
+    assert middleware.use_this_middleware()
+
+    with pytest.raises(PostingInterrupt):
         middleware.interrupt_posting(None)
 
-        self.assertIsNotNone(self.user.last_posted_on)
 
-    def test_flood_protection_middleware_old_posts(self):
-        """middleware is not interrupting if previous post is old"""
-        self.user.update_fields = []
+def test_middleware_lets_users_next_post_through_if_previous_post_is_not_recent(
+    dynamic_settings, user
+):
+    user.last_posted_on = timezone.now() - timedelta(seconds=10)
+    user.update_fields = []
+
+    middleware = FloodProtectionMiddleware(
+        mode=PostingEndpoint.START,
+        settings=dynamic_settings,
+        user=user,
+        user_acl=default_acl,
+    )
+    assert middleware.use_this_middleware()
+    middleware.interrupt_posting(None)
+
+
+def test_middleware_is_not_used_if_user_has_permission_to_omit_flood_protection(
+    dynamic_settings, user
+):
+    user.last_posted_on = timezone.now()
+    user.update_fields = []
+
+    middleware = FloodProtectionMiddleware(
+        mode=PostingEndpoint.START,
+        settings=dynamic_settings,
+        user=user,
+        user_acl=can_omit_flood_acl,
+    )
+    assert not middleware.use_this_middleware()
 
-        original_last_posted_on = timezone.now() - timedelta(days=1)
-        self.user.last_posted_on = original_last_posted_on
 
-        middleware = FloodProtectionMiddleware(user=self.user, user_acl=user_acl)
+def test_middleware_is_not_used_if_user_edits_post(dynamic_settings, user):
+    middleware = FloodProtectionMiddleware(
+        mode=PostingEndpoint.EDIT,
+        settings=dynamic_settings,
+        user=user,
+        user_acl=can_omit_flood_acl,
+    )
+    assert not middleware.use_this_middleware()
+
+
+@override_dynamic_settings(hourly_post_limit=3)
+def test_middleware_interrupts_posting_if_hourly_limit_was_met(
+    default_category, dynamic_settings, user
+):
+    user.update_fields = []
+
+    for _ in range(3):
+        post_thread(default_category, poster=user)
+
+    middleware = FloodProtectionMiddleware(
+        mode=PostingEndpoint.START,
+        settings=dynamic_settings,
+        user=user,
+        user_acl=default_acl,
+    )
+    assert middleware.use_this_middleware()
+
+    with pytest.raises(PostingInterrupt):
         middleware.interrupt_posting(None)
 
-        self.assertTrue(self.user.last_posted_on > original_last_posted_on)
 
-    def test_flood_protection_middleware_on_flood(self):
-        """middleware is interrupting flood"""
-        self.user.last_posted_on = timezone.now()
+@override_dynamic_settings(hourly_post_limit=0)
+def test_old_posts_dont_count_to_hourly_limit(default_category, dynamic_settings, user):
+    user.update_fields = []
+
+    for _ in range(3):
+        post_thread(
+            default_category,
+            poster=user,
+            started_on=timezone.now() - timedelta(hours=1),
+        )
+
+    middleware = FloodProtectionMiddleware(
+        mode=PostingEndpoint.START,
+        settings=dynamic_settings,
+        user=user,
+        user_acl=default_acl,
+    )
+    middleware.interrupt_posting(None)
 
-        with self.assertRaises(PostingInterrupt):
-            middleware = FloodProtectionMiddleware(user=self.user, user_acl=user_acl)
-            middleware.interrupt_posting(None)
 
-    def test_flood_permission(self):
-        """middleware is respects permission to flood for team members"""
-        can_omit_flood_protection_user_acl = {"can_omit_flood_protection": True}
-        middleware = FloodProtectionMiddleware(
-            user=self.user, user_acl=can_omit_flood_protection_user_acl
+@override_dynamic_settings(hourly_post_limit=0)
+def test_middleware_lets_post_through_if_hourly_limit_was_disabled(
+    default_category, dynamic_settings, user
+):
+    user.update_fields = []
+
+    for _ in range(3):
+        post_thread(default_category, poster=user)
+
+    middleware = FloodProtectionMiddleware(
+        mode=PostingEndpoint.START,
+        settings=dynamic_settings,
+        user=user,
+        user_acl=default_acl,
+    )
+    middleware.interrupt_posting(None)
+
+
+@override_dynamic_settings(daily_post_limit=3)
+def test_middleware_interrupts_posting_if_daily_limit_was_met(
+    default_category, dynamic_settings, user
+):
+    user.update_fields = []
+
+    for _ in range(3):
+        post_thread(default_category, poster=user)
+
+    middleware = FloodProtectionMiddleware(
+        mode=PostingEndpoint.START,
+        settings=dynamic_settings,
+        user=user,
+        user_acl=default_acl,
+    )
+    assert middleware.use_this_middleware()
+
+    with pytest.raises(PostingInterrupt):
+        middleware.interrupt_posting(None)
+
+
+@override_dynamic_settings(daily_post_limit=0)
+def test_old_posts_dont_count_to_daily_limit(default_category, dynamic_settings, user):
+    user.update_fields = []
+
+    for _ in range(3):
+        post_thread(
+            default_category, poster=user, started_on=timezone.now() - timedelta(days=1)
         )
-        self.assertFalse(middleware.use_this_middleware())
+
+    middleware = FloodProtectionMiddleware(
+        mode=PostingEndpoint.START,
+        settings=dynamic_settings,
+        user=user,
+        user_acl=default_acl,
+    )
+    middleware.interrupt_posting(None)
+
+
+@override_dynamic_settings(daily_post_limit=0)
+def test_middleware_lets_post_through_if_daily_limit_was_disabled(
+    default_category, dynamic_settings, user
+):
+    user.update_fields = []
+
+    for _ in range(3):
+        post_thread(default_category, poster=user)
+
+    middleware = FloodProtectionMiddleware(
+        mode=PostingEndpoint.START,
+        settings=dynamic_settings,
+        user=user,
+        user_acl=default_acl,
+    )
+    middleware.interrupt_posting(None)

+ 53 - 17
misago/threads/tests/test_gotoviews.py

@@ -2,7 +2,7 @@ from django.utils import timezone
 
 from .. import test
 from ...categories.models import Category
-from ...conf import settings
+from ...conf.test import override_dynamic_settings
 from ...readtracker.poststracker import save_read
 from ...users.test import AuthenticatedUserTestCase
 from ..test import patch_category_acl
@@ -10,6 +10,9 @@ from ..test import patch_category_acl
 GOTO_URL = "%s#post-%s"
 GOTO_PAGE_URL = "%s%s/#post-%s"
 
+POSTS_PER_PAGE = 7
+POSTS_PER_PAGE_ORPHANS = 3
+
 
 class GotoViewTestCase(AuthenticatedUserTestCase):
     def setUp(self):
@@ -32,9 +35,12 @@ class GotoPostTests(GotoViewTestCase):
         response = self.client.get(response["location"])
         self.assertContains(response, self.thread.first_post.get_absolute_url())
 
+    @override_dynamic_settings(
+        posts_per_page=POSTS_PER_PAGE, posts_per_page_orphans=POSTS_PER_PAGE_ORPHANS
+    )
     def test_goto_last_post_on_page(self):
         """last post on page redirect url is valid"""
-        for _ in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL - 1):
+        for _ in range(POSTS_PER_PAGE + POSTS_PER_PAGE_ORPHANS - 1):
             post = test.reply_thread(self.thread)
 
         response = self.client.get(post.get_absolute_url())
@@ -46,9 +52,12 @@ class GotoPostTests(GotoViewTestCase):
         response = self.client.get(response["location"])
         self.assertContains(response, post.get_absolute_url())
 
+    @override_dynamic_settings(
+        posts_per_page=POSTS_PER_PAGE, posts_per_page_orphans=POSTS_PER_PAGE_ORPHANS
+    )
     def test_goto_first_post_on_next_page(self):
         """first post on next page redirect url is valid"""
-        for _ in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL):
+        for _ in range(POSTS_PER_PAGE + POSTS_PER_PAGE_ORPHANS):
             post = test.reply_thread(self.thread)
 
         response = self.client.get(post.get_absolute_url())
@@ -61,14 +70,17 @@ class GotoPostTests(GotoViewTestCase):
         response = self.client.get(response["location"])
         self.assertContains(response, post.get_absolute_url())
 
+    @override_dynamic_settings(
+        posts_per_page=POSTS_PER_PAGE, posts_per_page_orphans=POSTS_PER_PAGE_ORPHANS
+    )
     def test_goto_first_post_on_page_three_out_of_five(self):
         """first post on next page redirect url is valid"""
         posts = []
-        for _ in range(settings.MISAGO_POSTS_PER_PAGE * 4 - 1):
+        for _ in range(POSTS_PER_PAGE * 4 - 1):
             post = test.reply_thread(self.thread)
             posts.append(post)
 
-        post = posts[settings.MISAGO_POSTS_PER_PAGE * 2 - 3]
+        post = posts[POSTS_PER_PAGE * 2 - 3]
 
         response = self.client.get(post.get_absolute_url())
         self.assertEqual(response.status_code, 302)
@@ -80,14 +92,17 @@ class GotoPostTests(GotoViewTestCase):
         response = self.client.get(response["location"])
         self.assertContains(response, post.get_absolute_url())
 
+    @override_dynamic_settings(
+        posts_per_page=POSTS_PER_PAGE, posts_per_page_orphans=POSTS_PER_PAGE_ORPHANS
+    )
     def test_goto_first_event_on_page_three_out_of_five(self):
         """event redirect url is valid"""
         posts = []
-        for _ in range(settings.MISAGO_POSTS_PER_PAGE * 4 - 1):
+        for _ in range(POSTS_PER_PAGE * 4 - 1):
             post = test.reply_thread(self.thread)
             posts.append(post)
 
-        post = posts[settings.MISAGO_POSTS_PER_PAGE * 2 - 2]
+        post = posts[POSTS_PER_PAGE * 2 - 2]
 
         self.thread.has_events = True
         self.thread.save()
@@ -119,9 +134,12 @@ class GotoLastTests(GotoViewTestCase):
         response = self.client.get(response["location"])
         self.assertContains(response, self.thread.last_post.get_absolute_url())
 
+    @override_dynamic_settings(
+        posts_per_page=POSTS_PER_PAGE, posts_per_page_orphans=POSTS_PER_PAGE_ORPHANS
+    )
     def test_goto_last_post_on_page(self):
         """last post on page redirect url is valid"""
-        for _ in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL - 1):
+        for _ in range(POSTS_PER_PAGE + POSTS_PER_PAGE_ORPHANS - 1):
             post = test.reply_thread(self.thread)
 
         response = self.client.get(self.thread.get_last_post_url())
@@ -144,12 +162,15 @@ class GotoNewTests(GotoViewTestCase):
             GOTO_URL % (self.thread.get_absolute_url(), self.thread.first_post_id),
         )
 
+    @override_dynamic_settings(
+        posts_per_page=POSTS_PER_PAGE, posts_per_page_orphans=POSTS_PER_PAGE_ORPHANS
+    )
     def test_goto_first_new_post(self):
         """first unread post redirect url in already read thread is valid"""
         save_read(self.user, self.thread.first_post)
 
         post = test.reply_thread(self.thread, posted_on=timezone.now())
-        for _ in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL - 1):
+        for _ in range(POSTS_PER_PAGE + POSTS_PER_PAGE_ORPHANS - 1):
             test.reply_thread(self.thread, posted_on=timezone.now())
 
         response = self.client.get(self.thread.get_new_post_url())
@@ -158,16 +179,19 @@ class GotoNewTests(GotoViewTestCase):
             response["location"], GOTO_URL % (self.thread.get_absolute_url(), post.pk)
         )
 
+    @override_dynamic_settings(
+        posts_per_page=POSTS_PER_PAGE, posts_per_page_orphans=POSTS_PER_PAGE_ORPHANS
+    )
     def test_goto_first_new_post_on_next_page(self):
         """first unread post redirect url in already read multipage thread is valid"""
         save_read(self.user, self.thread.first_post)
 
-        for _ in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL):
+        for _ in range(POSTS_PER_PAGE + POSTS_PER_PAGE_ORPHANS):
             last_post = test.reply_thread(self.thread, posted_on=timezone.now())
             save_read(self.user, last_post)
 
         post = test.reply_thread(self.thread, posted_on=timezone.now())
-        for _ in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL - 1):
+        for _ in range(POSTS_PER_PAGE + POSTS_PER_PAGE_ORPHANS - 1):
             test.reply_thread(self.thread, posted_on=timezone.now())
 
         response = self.client.get(self.thread.get_new_post_url())
@@ -177,11 +201,14 @@ class GotoNewTests(GotoViewTestCase):
             GOTO_PAGE_URL % (self.thread.get_absolute_url(), 2, post.pk),
         )
 
+    @override_dynamic_settings(
+        posts_per_page=POSTS_PER_PAGE, posts_per_page_orphans=POSTS_PER_PAGE_ORPHANS
+    )
     def test_goto_first_new_post_in_read_thread(self):
         """goto new in read thread points to last post"""
         save_read(self.user, self.thread.first_post)
 
-        for _ in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL):
+        for _ in range(POSTS_PER_PAGE + POSTS_PER_PAGE_ORPHANS):
             post = test.reply_thread(self.thread, posted_on=timezone.now())
             save_read(self.user, post)
 
@@ -192,9 +219,12 @@ class GotoNewTests(GotoViewTestCase):
             GOTO_PAGE_URL % (self.thread.get_absolute_url(), 2, post.pk),
         )
 
+    @override_dynamic_settings(
+        posts_per_page=POSTS_PER_PAGE, posts_per_page_orphans=POSTS_PER_PAGE_ORPHANS
+    )
     def test_guest_goto_first_new_post_in_thread(self):
         """guest goto new in read thread points to last post"""
-        for _ in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL):
+        for _ in range(POSTS_PER_PAGE + POSTS_PER_PAGE_ORPHANS):
             post = test.reply_thread(self.thread, posted_on=timezone.now())
 
         self.logout_user()
@@ -217,16 +247,19 @@ class GotoBestAnswerTests(GotoViewTestCase):
             GOTO_URL % (self.thread.get_absolute_url(), self.thread.first_post_id),
         )
 
+    @override_dynamic_settings(
+        posts_per_page=POSTS_PER_PAGE, posts_per_page_orphans=POSTS_PER_PAGE_ORPHANS
+    )
     def test_view_handles_best_answer(self):
         """if thread has best answer, redirect to it"""
-        for _ in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL):
+        for _ in range(POSTS_PER_PAGE + POSTS_PER_PAGE_ORPHANS):
             test.reply_thread(self.thread, posted_on=timezone.now())
 
         best_answer = test.reply_thread(self.thread, posted_on=timezone.now())
         self.thread.set_best_answer(self.user, best_answer)
         self.thread.save()
 
-        for _ in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL - 1):
+        for _ in range(POSTS_PER_PAGE + POSTS_PER_PAGE_ORPHANS - 1):
             test.reply_thread(self.thread, posted_on=timezone.now())
 
         response = self.client.get(self.thread.get_best_answer_url())
@@ -259,16 +292,19 @@ class GotoUnapprovedTests(GotoViewTestCase):
             GOTO_URL % (self.thread.get_absolute_url(), self.thread.first_post_id),
         )
 
+    @override_dynamic_settings(
+        posts_per_page=POSTS_PER_PAGE, posts_per_page_orphans=POSTS_PER_PAGE_ORPHANS
+    )
     @patch_category_acl({"can_approve_content": True})
     def test_view_handles_unapproved_posts(self):
         """if thread has unapproved posts, redirect to first of them"""
-        for _ in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL):
+        for _ in range(POSTS_PER_PAGE + POSTS_PER_PAGE_ORPHANS):
             test.reply_thread(self.thread, posted_on=timezone.now())
 
         post = test.reply_thread(
             self.thread, is_unapproved=True, posted_on=timezone.now()
         )
-        for _ in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL - 1):
+        for _ in range(POSTS_PER_PAGE + POSTS_PER_PAGE_ORPHANS - 1):
             test.reply_thread(self.thread, posted_on=timezone.now())
 
         response = self.client.get(self.thread.get_unapproved_post_url())

+ 10 - 10
misago/threads/tests/test_privatethread_start_api.py

@@ -308,18 +308,18 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
                 {"title": ["Thread title should contain alpha-numeric characters."]},
             )
 
+    @override_dynamic_settings(forum_address="http://test.com/")
     def test_can_start_thread(self):
         """endpoint creates new thread"""
-        with override_dynamic_settings(forum_address="http://test.com/"):
-            response = self.client.post(
-                self.api_link,
-                data={
-                    "to": [self.other_user.username],
-                    "title": "Hello, I am test thread!",
-                    "post": "Lorem ipsum dolor met!",
-                },
-            )
-            self.assertEqual(response.status_code, 200)
+        response = self.client.post(
+            self.api_link,
+            data={
+                "to": [self.other_user.username],
+                "title": "Hello, I am test thread!",
+                "post": "Lorem ipsum dolor met!",
+            },
+        )
+        self.assertEqual(response.status_code, 200)
 
         thread = self.user.thread_set.all()[:1][0]
 

+ 4 - 2
misago/threads/tests/test_thread_bulkpatch_api.py

@@ -4,6 +4,7 @@ from django.urls import reverse
 
 from .. import test
 from ...categories.models import Category
+from ...conf.test import override_dynamic_settings
 from ..models import Thread
 from ..test import patch_category_acl, patch_other_category_acl
 from .test_threads_api import ThreadsApiTestCase
@@ -94,18 +95,19 @@ class BulkPatchSerializerTests(ThreadsBulkPatchApiTestCase):
             {"ids": {"0": ["Ensure this value is greater than or equal to 1."]}},
         )
 
+    @override_dynamic_settings(threads_per_page=5)
     def test_too_large_input(self):
         """api rejects too large input"""
         response = self.patch(
             self.api_link,
-            {"ids": [i + 1 for i in range(200)], "ops": [{} for i in range(200)]},
+            {"ids": [i + 1 for i in range(6)], "ops": [{} for i in range(200)]},
         )
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
             response.json(),
             {
-                "ids": ["Ensure this field has no more than 25 elements."],
+                "ids": ["No more than 5 threads can be updated at a single time."],
                 "ops": ["Ensure this field has no more than 10 elements."],
             },
         )

+ 2 - 2
misago/threads/tests/test_thread_model.py

@@ -401,9 +401,9 @@ class ThreadModelTests(TestCase):
         ThreadParticipant.objects.add_participants(self.thread, [user, other_user])
         self.assertEqual(self.thread.participants.count(), 2)
 
-        user.delete()
+        user.delete(anonymous_username="Deleted")
         Thread.objects.get(id=self.thread.id)
 
-        other_user.delete()
+        other_user.delete(anonymous_username="Deleted")
         with self.assertRaises(Thread.DoesNotExist):
             Thread.objects.get(id=self.thread.id)

+ 1 - 1
misago/threads/tests/test_thread_postbulkdelete_api.py

@@ -93,7 +93,7 @@ class PostBulkDeleteApiTests(ThreadsApiTestCase):
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
             response.json(),
-            {"detail": "No more than 24 posts can be deleted at single time."},
+            {"detail": "No more than 24 posts can be deleted at a single time."},
         )
 
     @patch_category_acl({"can_hide_posts": 2})

+ 4 - 2
misago/threads/tests/test_thread_postbulkpatch_api.py

@@ -4,6 +4,7 @@ from django.urls import reverse
 
 from .. import test
 from ...categories.models import Category
+from ...conf.test import override_dynamic_settings
 from ...users.test import AuthenticatedUserTestCase
 from ..models import Post, Thread
 from ..test import patch_category_acl
@@ -94,18 +95,19 @@ class BulkPatchSerializerTests(ThreadPostBulkPatchApiTestCase):
             {"ids": {"0": ["Ensure this value is greater than or equal to 1."]}},
         )
 
+    @override_dynamic_settings(posts_per_page=4, posts_per_page_orphans=3)
     def test_too_large_input(self):
         """api rejects too large input"""
         response = self.patch(
             self.api_link,
-            {"ids": [i + 1 for i in range(200)], "ops": [{} for i in range(200)]},
+            {"ids": [i + 1 for i in range(8)], "ops": [{} for i in range(200)]},
         )
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
             response.json(),
             {
-                "ids": ["Ensure this field has no more than 24 elements."],
+                "ids": ["No more than 7 posts can be updated at a single time."],
                 "ops": ["Ensure this field has no more than 10 elements."],
             },
         )

+ 4 - 6
misago/threads/tests/test_thread_postmerge_api.py

@@ -4,10 +4,10 @@ from django.urls import reverse
 
 from .. import test
 from ...categories.models import Category
+from ...conf.test import override_dynamic_settings
 from ...readtracker import poststracker
 from ...users.test import AuthenticatedUserTestCase
 from ..models import Post
-from ..serializers.moderation import POSTS_LIMIT
 from ..test import patch_category_acl
 
 
@@ -157,21 +157,19 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             {"detail": "You have to select at least two posts to merge."},
         )
 
+    @override_dynamic_settings(posts_per_page=5, posts_per_page_orphans=3)
     @patch_category_acl({"can_merge_posts": True})
     def test_merge_limit(self):
         """api rejects more posts than merge limit"""
         response = self.client.post(
             self.api_link,
-            json.dumps({"posts": list(range(POSTS_LIMIT + 1))}),
+            json.dumps({"posts": list(range(9))}),
             content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
             response.json(),
-            {
-                "detail": "No more than %s posts can be merged at single time."
-                % POSTS_LIMIT
-            },
+            {"detail": "No more than 8 posts can be merged at a single time."},
         )
 
     @patch_category_acl({"can_merge_posts": True})

+ 4 - 9
misago/threads/tests/test_thread_postmove_api.py

@@ -4,10 +4,10 @@ from django.urls import reverse
 
 from .. import test
 from ...categories.models import Category
+from ...conf.test import override_dynamic_settings
 from ...readtracker import poststracker
 from ...users.test import AuthenticatedUserTestCase
 from ..models import Thread
-from ..serializers.moderation import POSTS_LIMIT
 from ..test import patch_category_acl, patch_other_category_acl
 
 
@@ -266,6 +266,7 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
             response.json(), {"detail": "One or more post ids received were invalid."}
         )
 
+    @override_dynamic_settings(posts_per_page=5, posts_per_page_orphans=3)
     @patch_category_acl({"can_move_posts": True})
     def test_move_limit(self):
         """api rejects more posts than move limit"""
@@ -274,20 +275,14 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
         response = self.client.post(
             self.api_link,
             json.dumps(
-                {
-                    "new_thread": other_thread.get_absolute_url(),
-                    "posts": list(range(POSTS_LIMIT + 1)),
-                }
+                {"new_thread": other_thread.get_absolute_url(), "posts": list(range(9))}
             ),
             content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
             response.json(),
-            {
-                "detail": "No more than %s posts can be moved at single time."
-                % POSTS_LIMIT
-            },
+            {"detail": "No more than 8 posts can be moved at a single time."},
         )
 
     @patch_category_acl({"can_move_posts": True})

+ 4 - 6
misago/threads/tests/test_thread_postsplit_api.py

@@ -4,10 +4,10 @@ from django.urls import reverse
 
 from .. import test
 from ...categories.models import Category
+from ...conf.test import override_dynamic_settings
 from ...readtracker import poststracker
 from ...users.test import AuthenticatedUserTestCase
 from ..models import Post
-from ..serializers.moderation import POSTS_LIMIT
 from ..test import patch_category_acl, patch_other_category_acl
 
 
@@ -157,21 +157,19 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
             response.json(), {"detail": "One or more post ids received were invalid."}
         )
 
+    @override_dynamic_settings(posts_per_page=5, posts_per_page_orphans=3)
     @patch_category_acl({"can_move_posts": True})
     def test_split_limit(self):
         """api rejects more posts than split limit"""
         response = self.client.post(
             self.api_link,
-            json.dumps({"posts": list(range(POSTS_LIMIT + 1))}),
+            json.dumps({"posts": list(range(9))}),
             content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
             response.json(),
-            {
-                "detail": "No more than %s posts can be split at single time."
-                % POSTS_LIMIT
-            },
+            {"detail": "No more than 8 posts can be split at a single time."},
         )
 
     @patch_category_acl({"can_move_posts": True})

+ 4 - 6
misago/threads/tests/test_threads_bulkdelete_api.py

@@ -5,8 +5,8 @@ from django.urls import reverse
 from .. import test
 from ...categories import PRIVATE_THREADS_ROOT_NAME
 from ...categories.models import Category
+from ...conf.test import override_dynamic_settings
 from ..models import Thread
-from ..serializers.moderation import THREADS_LIMIT
 from ..test import patch_category_acl
 from ..threadtypes import trees_map
 from .test_threads_api import ThreadsApiTestCase
@@ -69,17 +69,15 @@ class ThreadsBulkDeleteApiTests(ThreadsApiTestCase):
             response.json(), {"detail": "One or more thread ids received were invalid."}
         )
 
+    @override_dynamic_settings(threads_per_page=4)
     @patch_category_acl({"can_hide_threads": 2, "can_hide_own_threads": 2})
     def test_validate_ids_length(self):
         """api validates that ids are list of ints"""
-        response = self.delete(self.api_link, list(range(THREADS_LIMIT + 1)))
+        response = self.delete(self.api_link, list(range(5)))
         self.assertEqual(response.status_code, 403)
         self.assertEqual(
             response.json(),
-            {
-                "detail": "No more than %s threads can be deleted at single time."
-                % THREADS_LIMIT
-            },
+            {"detail": "No more than 4 threads can be deleted at a single time."},
         )
 
     @patch_category_acl({"can_hide_threads": 2, "can_hide_own_threads": 2})

+ 4 - 6
misago/threads/tests/test_threads_merge_api.py

@@ -6,11 +6,11 @@ from .. import test
 from ...acl import useracl
 from ...acl.objectacl import add_acl_to_obj
 from ...categories.models import Category
+from ...conf.test import override_dynamic_settings
 from ...conftest import get_cache_versions
 from ...readtracker import poststracker
 from ..models import Poll, PollVote, Post, Thread
 from ..serializers import ThreadsListSerializer
-from ..serializers.moderation import THREADS_LIMIT
 from ..test import patch_category_acl, patch_other_category_acl
 from .test_threads_api import ThreadsApiTestCase
 
@@ -218,11 +218,12 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             ],
         )
 
+    @override_dynamic_settings(threads_per_page=4)
     @patch_category_acl({"can_merge_threads": True})
     def test_merge_too_many_threads(self):
         """api rejects too many threads to merge"""
         threads = []
-        for _ in range(THREADS_LIMIT + 1):
+        for _ in range(5):
             threads.append(test.post_thread(category=self.category).pk)
 
         response = self.client.post(
@@ -233,10 +234,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(response.status_code, 403)
         self.assertEqual(
             response.json(),
-            {
-                "detail": "No more than %s threads can be merged at single time."
-                % THREADS_LIMIT
-            },
+            {"detail": "No more than 4 threads can be merged at a single time."},
         )
 
     @patch_category_acl({"can_merge_threads": True})

+ 10 - 10
misago/threads/tests/test_threadslists.py

@@ -8,6 +8,7 @@ from .. import test
 from ...acl.test import patch_user_acl
 from ...categories.models import Category
 from ...conf import settings
+from ...conf.test import override_dynamic_settings
 from ...readtracker import poststracker
 from ...users.test import AuthenticatedUserTestCase
 
@@ -332,16 +333,17 @@ class AllThreadsListTests(ThreadsListTestCase):
         self.assertTrue(positions["l"] > positions["g"])
         self.assertTrue(positions["l"] > positions["s"])
 
+    @override_dynamic_settings(threads_per_page=5)
     def test_noscript_pagination(self):
         """threads list is paginated for users with js disabled"""
-        threads_per_page = settings.MISAGO_THREADS_PER_PAGE
+        threads_per_page = 5
 
         # post and discard thread to move last_post_id count by one
         test.post_thread(category=self.first_category).delete()
 
         # create test threads
         threads = []
-        for _ in range(settings.MISAGO_THREADS_PER_PAGE * 2):
+        for _ in range(threads_per_page * 2):
             threads.append(test.post_thread(category=self.first_category))
 
         # threads starting with given one are on the list
@@ -355,11 +357,11 @@ class AllThreadsListTests(ThreadsListTestCase):
         self.assertContainsThread(response, threads[-2])
 
         # slice contains expected threads
-        for visible_thread in threads[settings.MISAGO_THREADS_PER_PAGE - 1 : -1]:
+        for visible_thread in threads[threads_per_page - 1 : -1]:
             self.assertContainsThread(response, visible_thread)
 
         # threads after slice are hidden
-        for invisible_thread in threads[: settings.MISAGO_THREADS_PER_PAGE - 1]:
+        for invisible_thread in threads[: threads_per_page - 1]:
             self.assertNotContainsThread(response, invisible_thread)
 
         # nonexisting start gives 404
@@ -825,6 +827,7 @@ class NewThreadsListTests(ThreadsListTestCase):
         self.assertEqual(len(response_json["results"]), 1)
         self.assertEqual(response_json["results"][0]["id"], test_thread.pk)
 
+    @override_dynamic_settings(readtracker_cutoff=3)
     @patch_categories_acl()
     def test_list_hides_global_cutoff_thread(self):
         """list hides thread started before global cutoff"""
@@ -832,9 +835,7 @@ class NewThreadsListTests(ThreadsListTestCase):
         self.user.save()
 
         test_thread = test.post_thread(
-            category=self.category_a,
-            started_on=timezone.now()
-            - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF + 1),
+            category=self.category_a, started_on=timezone.now() - timedelta(days=5)
         )
 
         response = self.client.get("/new/")
@@ -1052,6 +1053,7 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         response_json = response.json()
         self.assertEqual(len(response_json["results"]), 0)
 
+    @override_dynamic_settings(readtracker_cutoff=3)
     @patch_categories_acl()
     def test_list_hides_global_cutoff_thread(self):
         """list hides thread replied before global cutoff"""
@@ -1059,9 +1061,7 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         self.user.save()
 
         test_thread = test.post_thread(
-            category=self.category_a,
-            started_on=timezone.now()
-            - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF + 5),
+            category=self.category_a, started_on=timezone.now() - timedelta(days=5)
         )
 
         poststracker.save_read(self.user, test_thread.first_post)

+ 8 - 9
misago/threads/tests/test_threadview.py

@@ -4,7 +4,7 @@ from .. import test
 from ...acl import useracl
 from ...acl.test import patch_user_acl
 from ...categories.models import Category
-from ...conf import settings
+from ...conf.test import override_dynamic_settings
 from ...conftest import get_cache_versions
 from ...users.test import AuthenticatedUserTestCase
 from ..checksums import update_post_checksum
@@ -280,12 +280,11 @@ class ThreadEventVisibilityTests(ThreadViewTestCase):
                 response = self.client.get(self.thread.get_absolute_url())
                 self.assertNotContains(response, event.get_absolute_url())
 
+    @override_dynamic_settings(events_per_page=4)
     def test_events_limit(self):
         """forum will trim oldest events if theres more than allowed by config"""
-        events_limit = settings.MISAGO_EVENTS_PER_PAGE
         events = []
-
-        for _ in range(events_limit + 5):
+        for _ in range(5):
             request = Mock(user=self.user, user_ip="127.0.0.1")
             event = record_event(request, self.thread, "closed")
             events.append(event)
@@ -293,15 +292,15 @@ class ThreadEventVisibilityTests(ThreadViewTestCase):
         # test that only events within limits were rendered
         response = self.client.get(self.thread.get_absolute_url())
 
-        for event in events[5:]:
+        for event in events[4:]:
             self.assertContains(response, event.get_absolute_url())
-        for event in events[:5]:
-            self.assertNotContains(response, event.get_absolute_url())
+        self.assertNotContains(response, events[0].get_absolute_url())
 
+    @override_dynamic_settings(posts_per_page=10, events_per_page=4)
     def test_events_dont_take_space(self):
         """events dont take space away from posts"""
-        posts_limit = settings.MISAGO_POSTS_PER_PAGE
-        events_limit = settings.MISAGO_EVENTS_PER_PAGE
+        posts_limit = 10
+        events_limit = 4
         events = []
 
         for _ in range(events_limit + 5):

+ 4 - 5
misago/threads/viewmodels/posts.py

@@ -1,5 +1,4 @@
 from ...acl.objectacl import add_acl_to_obj
-from ...conf import settings
 from ...core.shortcuts import paginate, pagination_dict
 from ...readtracker.poststracker import make_read_aware
 from ...users.online.utils import make_users_status_aware
@@ -20,8 +19,8 @@ class ViewModel:
 
         posts_queryset = self.get_posts_queryset(request, thread_model)
 
-        posts_limit = settings.MISAGO_POSTS_PER_PAGE
-        posts_orphans = settings.MISAGO_POSTS_TAIL
+        posts_limit = request.settings.posts_per_page
+        posts_orphans = request.settings.posts_per_page_orphans
         list_page = paginate(
             posts_queryset, page, posts_limit, posts_orphans, paginator=PostsPaginator
         )
@@ -51,7 +50,7 @@ class ViewModel:
             if list_page.has_next():
                 last_post = posts[-1]
 
-            events_limit = settings.MISAGO_EVENTS_PER_PAGE
+            events_limit = request.settings.events_per_page
             posts += self.get_events_queryset(
                 request, thread_model, events_limit, first_post, last_post
             )
@@ -61,7 +60,7 @@ class ViewModel:
 
         # make posts and events ACL and reads aware
         add_acl_to_obj(request.user_acl, posts)
-        make_read_aware(request.user, posts)
+        make_read_aware(request, posts)
 
         self._user = request.user
 

+ 1 - 1
misago/threads/viewmodels/thread.py

@@ -50,7 +50,7 @@ class ViewModel(BaseViewModel):
         add_acl_to_obj(request.user_acl, model)
 
         if read_aware:
-            make_read_aware(request.user, request.user_acl, model)
+            make_read_aware(request, model)
         if subscription_aware:
             make_subscription_aware(request.user, model)
 

+ 8 - 9
misago/threads/viewmodels/threads.py

@@ -6,10 +6,9 @@ from django.utils.translation import gettext as _
 from django.utils.translation import gettext_lazy
 
 from ...acl.objectacl import add_acl_to_obj
-from ...conf import settings
 from ...core.cursorpagination import get_page
 from ...readtracker import threadstracker
-from ...readtracker.dates import get_cutoff_date
+from ...readtracker.cutoffdate import get_cutoff_date
 from ..models import Post, Thread
 from ..participants import make_participants_aware
 from ..permissions import exclude_invisible_posts, exclude_invisible_threads
@@ -64,7 +63,7 @@ class ViewModel:
             list_page = get_page(
                 threads_queryset,
                 "-last_post_id",
-                settings.MISAGO_THREADS_PER_PAGE,
+                request.settings.threads_per_page,
                 start,
             )
         except (EmptyPage, InvalidPage):
@@ -90,7 +89,7 @@ class ViewModel:
                 thread.is_read = False
                 thread.is_new = True
         else:
-            threadstracker.make_read_aware(request.user, request.user_acl, threads)
+            threadstracker.make_read_aware(request, threads)
 
         self.filter_threads(request, threads)
 
@@ -208,16 +207,14 @@ def filter_threads_queryset(request, categories, list_type, queryset):
 
 def filter_read_threads_queryset(request, categories, list_type, queryset):
     # grab cutoffs for categories
-    user = request.user
-
-    cutoff_date = get_cutoff_date(user)
+    cutoff_date = get_cutoff_date(request.settings, request.user)
 
     visible_posts = Post.objects.filter(posted_on__gt=cutoff_date)
     visible_posts = exclude_invisible_posts(request.user_acl, categories, visible_posts)
 
     queryset = queryset.filter(id__in=visible_posts.distinct().values("thread"))
 
-    read_posts = visible_posts.filter(id__in=user.postread_set.values("post"))
+    read_posts = visible_posts.filter(id__in=request.user.postread_set.values("post"))
 
     if list_type == "new":
         # new threads have no entry in reads table
@@ -225,7 +222,9 @@ def filter_read_threads_queryset(request, categories, list_type, queryset):
 
     if list_type == "unread":
         # unread threads were read in past but have new posts
-        unread_posts = visible_posts.exclude(id__in=user.postread_set.values("post"))
+        unread_posts = visible_posts.exclude(
+            id__in=request.user.postread_set.values("post")
+        )
         queryset = queryset.filter(id__in=read_posts.distinct().values("thread"))
         queryset = queryset.filter(id__in=unread_posts.distinct().values("thread"))
         return queryset

+ 8 - 7
misago/threads/views/attachment.py

@@ -1,3 +1,4 @@
+from django.templatetags.static import static
 from django.core.exceptions import PermissionDenied
 from django.http import Http404
 from django.shortcuts import get_object_or_404, redirect
@@ -5,18 +6,18 @@ from django.shortcuts import get_object_or_404, redirect
 from ...conf import settings
 from ..models import Attachment, AttachmentType
 
-ATTACHMENT_404_URL = "".join((settings.STATIC_URL, settings.MISAGO_404_IMAGE))
-ATTACHMENT_403_URL = "".join((settings.STATIC_URL, settings.MISAGO_403_IMAGE))
+DEFAULT_403_URL = static(settings.MISAGO_ATTACHMENT_403_IMAGE)
+DEFAULT_404_URL = static(settings.MISAGO_ATTACHMENT_404_IMAGE)
 
 
 def attachment_server(request, pk, secret, thumbnail=False):
     try:
         url = serve_file(request, pk, secret, thumbnail)
         return redirect(url)
-    except Http404:
-        return redirect(ATTACHMENT_404_URL)
     except PermissionDenied:
-        return redirect(ATTACHMENT_403_URL)
+        return redirect(request.settings.attachment_403_image or DEFAULT_403_URL)
+    except Http404:
+        return redirect(request.settings.attachment_404_image or DEFAULT_404_URL)
 
 
 def serve_file(request, pk, secret, thumbnail):
@@ -24,7 +25,7 @@ def serve_file(request, pk, secret, thumbnail):
     attachment = get_object_or_404(queryset, pk=pk, secret=secret)
 
     if not attachment.post_id and request.GET.get("shva") != "1":
-        # if attachment is orphaned, don't run acl test unless explictly told so
+        # if attachment is orphaned, don't run acl test unless explicitly told so
         # this saves user suprise of deleted attachment still showing in posts/quotes
         raise Http404()
 
@@ -32,7 +33,7 @@ def serve_file(request, pk, secret, thumbnail):
         allow_file_download(request, attachment)
 
     if attachment.is_image:
-        if thumbnail:
+        if thumbnail and attachment.thumbnail:
             return attachment.thumbnail.url
         return attachment.image.url
 

+ 5 - 4
misago/threads/views/goto.py

@@ -7,7 +7,7 @@ from django.utils.translation import gettext as _
 from django.views import View
 
 from ...conf import settings
-from ...readtracker.dates import get_cutoff_date
+from ...readtracker.cutoffdate import get_cutoff_date
 from ..permissions import exclude_invisible_posts
 from ..viewmodels import ForumThread, PrivateThread
 
@@ -55,8 +55,8 @@ class GotoView(View):
 
         post_position = previous_posts.count()
 
-        per_page = settings.MISAGO_POSTS_PER_PAGE - 1
-        orphans = settings.MISAGO_POSTS_TAIL
+        per_page = self.request.settings.posts_per_page - 1
+        orphans = self.request.settings.posts_per_page_orphans
         if orphans:
             orphans += 1
 
@@ -90,7 +90,8 @@ class ThreadGotoLastView(GotoView):
 class GetFirstUnreadPostMixin:
     def get_first_unread_post(self, user, posts_queryset):
         if user.is_authenticated:
-            expired_posts = Q(posted_on__lt=get_cutoff_date(user))
+            cutoff_date = get_cutoff_date(self.request.settings, user)
+            expired_posts = Q(posted_on__lt=cutoff_date)
             read_posts = Q(id__in=user.postread_set.values("post"))
 
             first_unread = (

+ 5 - 3
misago/users/activepostersranking.py

@@ -5,7 +5,7 @@ from django.db.models import Count
 from django.utils import timezone
 
 from ..categories.models import Category
-from ..conf import settings
+from ..conf.shortcuts import get_dynamic_settings
 from .models import ActivityRanking
 
 User = get_user_model()
@@ -23,7 +23,8 @@ def get_active_posters_ranking():
 
 
 def build_active_posters_ranking():
-    tracked_period = settings.MISAGO_RANKING_LENGTH
+    settings = get_dynamic_settings()
+    tracked_period = settings.top_posters_ranking_length
     tracked_since = timezone.now() - timedelta(days=tracked_period)
 
     ActivityRanking.objects.all().delete()
@@ -41,9 +42,10 @@ def build_active_posters_ranking():
         .annotate(score=Count("post"))
         .filter(score__gt=0)
         .order_by("-score")
-    )[: settings.MISAGO_RANKING_SIZE]
+    )[: settings.top_posters_ranking_size]
 
     new_ranking = []
     for ranking in queryset.iterator():
         new_ranking.append(ActivityRanking(user=ranking, score=ranking.score))
     ActivityRanking.objects.bulk_create(new_ranking)
+    return new_ranking

+ 1 - 1
misago/users/admin/forms.py

@@ -638,7 +638,7 @@ class RequestDataDownloadsForm(forms.Form):
         if len(user_identifiers) > 20:
             raise forms.ValidationError(
                 _(
-                    "You may not enter more than 20 items at single time "
+                    "You may not enter more than 20 items at a single time "
                     "(You have entered %(show_value)s)."
                 )
                 % {"show_value": len(user_identifiers)}

+ 4 - 1
misago/users/admin/tasks.py

@@ -1,6 +1,8 @@
 from celery import shared_task
 from django.contrib.auth import get_user_model
 
+from ...conf.shortcuts import get_dynamic_settings
+
 User = get_user_model()
 
 
@@ -11,4 +13,5 @@ def delete_user_with_content(pk):
     except User.DoesNotExist:
         pass
     else:
-        user.delete(delete_content=True)
+        settings = get_dynamic_settings()
+        user.delete(anonymous_username=settings.anonymous_username, delete_content=True)

+ 1 - 1
misago/users/admin/views/users.py

@@ -218,7 +218,7 @@ class UsersList(UserAdmin, generic.ListView):
                 raise generic.MassActionError(message)
 
         for user in users:
-            user.delete()
+            user.delete(anonymous_username=request.settings.anonymous_username)
 
         messages.success(request, _("Selected users have been deleted."))
 

+ 5 - 4
misago/users/api/users.py

@@ -12,7 +12,6 @@ from rest_framework.response import Response
 
 from ...acl.objectacl import add_acl_to_obj
 from ...categories.models import Category
-from ...conf import settings
 from ...core.rest_permissions import IsAuthenticatedOrReadOnly
 from ...core.shortcuts import get_int_or_404
 from ...threads.moderation import hide_post, hide_thread
@@ -269,12 +268,14 @@ class UserViewSet(viewsets.GenericViewSet):
             request.user, pk, _("You can't request data downloads for other users.")
         )
 
-        if not settings.MISAGO_ENABLE_DOWNLOAD_OWN_DATA:
+        if not request.settings.allow_data_downloads:
             raise PermissionDenied(_("You can't download your data."))
 
         if user_has_data_download_request(request.user):
             raise PermissionDenied(
-                _("You can't have more than one data download request at single time.")
+                _(
+                    "You can't have more than one data download request at a single time."
+                )
             )
 
         request_user_data_download(request.user)
@@ -316,7 +317,7 @@ class UserViewSet(viewsets.GenericViewSet):
                         category.synchronize()
                         category.save()
 
-                profile.delete()
+                profile.delete(anonymous_username=request.settings.anonymous_username)
 
         return Response({})
 

+ 20 - 14
misago/users/apps.py

@@ -44,21 +44,27 @@ class MisagoUsersConfig(AppConfig):
             icon="vpn_key",
         )
 
-        if settings.MISAGO_ENABLE_DOWNLOAD_OWN_DATA:
-            usercp.add_section(
-                link="misago:usercp-download-data",
-                name=_("Download data"),
-                component="download-data",
-                icon="save_alt",
-            )
+        def can_download_own_data(request):
+            return request.settings.allow_data_downloads
 
-        if settings.MISAGO_ENABLE_DELETE_OWN_ACCOUNT:
-            usercp.add_section(
-                link="misago:usercp-delete-account",
-                name=_("Delete account"),
-                component="delete-account",
-                icon="cancel",
-            )
+        usercp.add_section(
+            link="misago:usercp-download-data",
+            name=_("Download data"),
+            component="download-data",
+            icon="save_alt",
+            visible_if=can_download_own_data,
+        )
+
+        def can_delete_own_account(request):
+            return request.settings.allow_delete_own_account
+
+        usercp.add_section(
+            link="misago:usercp-delete-account",
+            name=_("Delete account"),
+            component="delete-account",
+            icon="cancel",
+            visible_if=can_delete_own_account,
+        )
 
     def register_default_users_list_pages(self):
         users_list.add_section(

+ 3 - 5
misago/users/datadownloads/__init__.py

@@ -25,23 +25,21 @@ def request_user_data_download(user, requester=None):
     )
 
 
-def prepare_user_data_download(download, logger=None):
+def prepare_user_data_download(download, expires_in, logger=None):
     working_dir = settings.MISAGO_USER_DATA_DOWNLOADS_WORKING_DIR
     user = download.user
     with DataArchive(user, working_dir) as archive:
         try:
             archive_user_data.send(user, archive=archive)
             download.status = DataDownload.STATUS_READY
-            download.expires_on = timezone.now() + timedelta(
-                hours=settings.MISAGO_USER_DATA_DOWNLOADS_EXPIRE_IN_HOURS
-            )
+            download.expires_on = timezone.now() + timedelta(hours=expires_in)
             download.file = archive.get_file()
             download.save()
             # todo: send an e-mail with download link
             return True
         except Exception as e:  # pylint: disable=broad-except
             if logger:
-                logger.exception(e)
+                logger.exception(e, exc_info=e)
             return False
 
 

+ 2 - 4
misago/users/management/commands/createsuperuser.py

@@ -12,8 +12,7 @@ from django.core.management.base import BaseCommand
 from django.db import DEFAULT_DB_ALIAS, IntegrityError
 from django.utils.encoding import force_str
 
-from ....cache.versions import get_cache_versions
-from ....conf.dynamicsettings import DynamicSettings
+from ....conf.shortcuts import get_dynamic_settings
 from ...setupnewuser import setup_new_user
 from ...validators import validate_email, validate_username
 
@@ -82,8 +81,7 @@ class Command(BaseCommand):
         interactive = options.get("interactive")
         verbosity = int(options.get("verbosity", 1))
 
-        cache_versions = get_cache_versions()
-        settings = DynamicSettings(cache_versions)
+        settings = get_dynamic_settings()
 
         # Validate initial inputs
         if username is not None:

+ 7 - 6
misago/users/management/commands/deleteinactiveusers.py

@@ -4,7 +4,7 @@ from django.contrib.auth import get_user_model
 from django.core.management.base import BaseCommand
 from django.utils import timezone
 
-from ....conf import settings
+from ....conf.shortcuts import get_dynamic_settings
 from ....core.pgutils import chunk_queryset
 
 User = get_user_model()
@@ -14,16 +14,17 @@ class Command(BaseCommand):
     help = "Deletes inactive user accounts older than set time."
 
     def handle(self, *args, **options):
-        if not settings.MISAGO_DELETE_NEW_INACTIVE_USERS_OLDER_THAN_DAYS:
+        settings = get_dynamic_settings()
+        if not settings.new_inactive_accounts_delete:
             self.stdout.write(
-                "Automatic deletion of inactive users is currently disabled."
+                "Automatic deletion of inactive user accounts is currently disabled."
             )
             return
 
         users_deleted = 0
 
         joined_on_cutoff = timezone.now() - timedelta(
-            days=settings.MISAGO_DELETE_NEW_INACTIVE_USERS_OLDER_THAN_DAYS
+            days=settings.new_inactive_accounts_delete
         )
 
         queryset = User.objects.filter(
@@ -31,7 +32,7 @@ class Command(BaseCommand):
         )
 
         for user in chunk_queryset(queryset):
-            user.delete()
+            user.delete(anonymous_username=settings.anonymous_username)
             users_deleted += 1
 
-        self.stdout.write("Deleted users: %s" % users_deleted)
+        self.stdout.write("Deleted inactive user accounts: %s" % users_deleted)

+ 4 - 2
misago/users/management/commands/deletemarkedusers.py

@@ -1,6 +1,7 @@
 from django.contrib.auth import get_user_model
 from django.core.management.base import BaseCommand
 
+from ....conf.shortcuts import get_dynamic_settings
 from ....core.pgutils import chunk_queryset
 from ...permissions import can_delete_own_account
 
@@ -15,12 +16,13 @@ class Command(BaseCommand):
 
     def handle(self, *args, **options):
         users_deleted = 0
+        settings = get_dynamic_settings()
 
         queryset = User.objects.filter(is_deleting_account=True)
 
         for user in chunk_queryset(queryset):
-            if can_delete_own_account(user, user):
-                user.delete()
+            if can_delete_own_account(settings, user, user):
+                user.delete(anonymous_username=settings.anonymous_username)
                 users_deleted += 1
 
         self.stdout.write("Deleted users: %s" % users_deleted)

+ 4 - 6
misago/users/management/commands/prepareuserdatadownloads.py

@@ -3,9 +3,8 @@ import logging
 from django.core.management.base import BaseCommand
 from django.utils.translation import gettext
 
-from ....cache.versions import get_cache_versions
 from ....conf import settings
-from ....conf.dynamicsettings import DynamicSettings
+from ....conf.shortcuts import get_dynamic_settings
 from ....core.mail import mail_user
 from ....core.pgutils import chunk_queryset
 from ...datadownloads import prepare_user_data_download
@@ -27,15 +26,14 @@ class Command(BaseCommand):
             )
             return
 
-        cache_versions = get_cache_versions()
-        dynamic_settings = DynamicSettings(cache_versions)
-        expires_in = settings.MISAGO_USER_DATA_DOWNLOADS_EXPIRE_IN_HOURS
+        dynamic_settings = get_dynamic_settings()
+        expires_in = dynamic_settings.data_downloads_expiration
 
         downloads_prepared = 0
         queryset = DataDownload.objects.select_related("user")
         queryset = queryset.filter(status=DataDownload.STATUS_PENDING)
         for data_download in chunk_queryset(queryset):
-            if prepare_user_data_download(data_download, logger):
+            if prepare_user_data_download(data_download, expires_in, logger):
                 user = data_download.user
                 subject = gettext("%(user)s, your data download is ready") % {
                     "user": user

+ 6 - 5
misago/users/management/commands/removeoldips.py

@@ -1,20 +1,21 @@
 from django.core.management import BaseCommand
 
-from ....conf import settings
+from ....conf.shortcuts import get_dynamic_settings
 from ...signals import remove_old_ips
 
 
 class Command(BaseCommand):
-    help = "Removes users IPs stored for longer than set in MISAGO_IP_STORE_TIME."
+    help = "Removes users IPs stored for longer than configured by administrator."
 
     def handle(self, *args, **options):
-        if not settings.MISAGO_IP_STORE_TIME:
+        settings = get_dynamic_settings()
+        if not settings.ip_storage_time:
             self.stdout.write("Old IP removal is disabled.")
             return
 
-        remove_old_ips.send(sender=self)
+        remove_old_ips.send(sender=self, ip_storage_time=settings.ip_storage_time)
 
         self.stdout.write(
             "IP addresses older than %s days have been removed."
-            % settings.MISAGO_IP_STORE_TIME
+            % settings.ip_storage_time
         )

+ 7 - 3
misago/users/models/user.py

@@ -252,7 +252,11 @@ class User(AbstractBaseUser, PermissionsMixin):
         if kwargs.pop("delete_content", False):
             self.delete_content()
 
-        self.anonymize_data()
+        username = kwargs.pop("anonymous_username", None)
+        if username:
+            self.anonymize_data(username)
+        else:
+            raise ValueError("user.delete() requires 'anonymous_username' argument")
 
         avatars.delete_avatar(self)
 
@@ -268,13 +272,13 @@ class User(AbstractBaseUser, PermissionsMixin):
         self.is_deleting_account = True
         self.save(update_fields=["is_active", "is_deleting_account"])
 
-    def anonymize_data(self):
+    def anonymize_data(self, anonymous_username):
         """Replaces username with anonymized one, then send anonymization signal.
 
         Items associated with this user then anonymize their user-specific data
         like username or IP addresses.
         """
-        self.username = settings.MISAGO_ANONYMOUS_USERNAME
+        self.username = anonymous_username
         self.slug = slugify(self.username)
 
         from ..signals import anonymize_user_data

+ 3 - 4
misago/users/permissions/delete.py

@@ -10,7 +10,6 @@ from django.utils.translation import ngettext
 from ...acl import algebra
 from ...acl.decorators import return_boolean
 from ...acl.models import Role
-from ...conf import settings
 
 __all__ = [
     "allow_delete_user",
@@ -100,11 +99,11 @@ def allow_delete_user(user_acl, target):
 can_delete_user = return_boolean(allow_delete_user)
 
 
-def allow_delete_own_account(user, target):
-    if not settings.MISAGO_ENABLE_DELETE_OWN_ACCOUNT and not user.is_deleting_account:
-        raise PermissionDenied(_("You can't delete your account."))
+def allow_delete_own_account(settings, user, target):
     if user.id != target.id:
         raise PermissionDenied(_("You can't delete other users accounts."))
+    if not settings.allow_delete_own_account and not user.is_deleting_account:
+        raise PermissionDenied(_("You can't delete your account."))
     if user.is_staff or user.is_superuser:
         raise PermissionDenied(
             _("You can't delete your account because you are an administrator.")

+ 1 - 1
misago/users/serializers/options.py

@@ -122,7 +122,7 @@ class DeleteOwnAccountSerializer(serializers.Serializer):
         deactivate it and sign user out.
         """
         profile = self.context["user"]
-        allow_delete_own_account(request.user, profile)
+        allow_delete_own_account(request.settings, request.user, profile)
 
         logout(request)
         clear_tracking(request)

+ 4 - 5
misago/users/signals.py

@@ -7,7 +7,6 @@ from django.dispatch import Signal, receiver
 from django.utils import timezone
 from django.utils.translation import gettext as _
 
-from ..conf import settings
 from ..core.pgutils import chunk_queryset
 from .models import AuditTrail, DataDownload
 from .profilefields import profilefields
@@ -86,8 +85,8 @@ def handle_name_change(sender, **kwargs):
 
 
 @receiver(remove_old_ips)
-def remove_old_registrations_ips(sender, **kwargs):
-    datetime_cutoff = timezone.now() - timedelta(days=settings.MISAGO_IP_STORE_TIME)
+def remove_old_registrations_ips(sender, *, ip_storage_time, **kwargs):
+    datetime_cutoff = timezone.now() - timedelta(days=ip_storage_time)
     ip_is_too_new = Q(joined_on__gt=datetime_cutoff)
     ip_is_already_removed = Q(joined_from_ip__isnull=True)
 
@@ -96,8 +95,8 @@ def remove_old_registrations_ips(sender, **kwargs):
 
 
 @receiver(remove_old_ips)
-def remove_old_audit_trails(sender, **kwargs):
-    removal_cutoff = timezone.now() - timedelta(days=settings.MISAGO_IP_STORE_TIME)
+def remove_old_audit_trails(sender, *, ip_storage_time, **kwargs):
+    removal_cutoff = timezone.now() - timedelta(days=ip_storage_time)
     AuditTrail.objects.filter(created_on__lte=removal_cutoff).delete()
 
 

+ 0 - 82
misago/users/tests/test_activepostersranking.py

@@ -1,82 +0,0 @@
-from datetime import timedelta
-
-from django.utils import timezone
-
-from ...categories.models import Category
-from ...threads.test import post_thread
-from ..activepostersranking import (
-    build_active_posters_ranking,
-    get_active_posters_ranking,
-)
-from ..test import AuthenticatedUserTestCase, create_test_user
-
-
-class TestActivePostersRanking(AuthenticatedUserTestCase):
-    def setUp(self):
-        super().setUp()
-
-        self.category = Category.objects.get(slug="first-category")
-
-    def test_get_active_posters_ranking(self):
-        """get_active_posters_ranking returns list of active posters"""
-        # no posts, empty tanking
-        empty_ranking = get_active_posters_ranking()
-
-        self.assertEqual(empty_ranking["users"], [])
-        self.assertEqual(empty_ranking["users_count"], 0)
-
-        # other user that will be posting
-        other_user = create_test_user("OtherUser", "otheruser@example.com")
-
-        # lurker user that won't post anything
-        create_test_user("Lurker", "lurker@example.com")
-
-        # unranked user that posted something 400 days ago
-        unranked_user = create_test_user("UnrankedUser", "unranked@example.com")
-
-        started_on = timezone.now() - timedelta(days=400)
-        post_thread(self.category, poster=unranked_user, started_on=started_on)
-
-        # Start testing scenarios
-        post_thread(self.category, poster=other_user)
-
-        build_active_posters_ranking()
-        ranking = get_active_posters_ranking()
-
-        self.assertEqual(ranking["users"], [other_user])
-        self.assertEqual(ranking["users_count"], 1)
-
-        # two users in ranking
-        post_thread(self.category, poster=self.user)
-        post_thread(self.category, poster=self.user)
-
-        build_active_posters_ranking()
-        ranking = get_active_posters_ranking()
-
-        self.assertEqual(ranking["users"], [self.user, other_user])
-        self.assertEqual(ranking["users_count"], 2)
-
-        self.assertEqual(ranking["users"][0].score, 2)
-        self.assertEqual(ranking["users"][1].score, 1)
-
-        # disabled users are not ranked
-        disabled = create_test_user("DisabledUser", "disableduser@example.com")
-
-        disabled.is_active = False
-        disabled.save()
-
-        post_thread(self.category, poster=disabled)
-        post_thread(self.category, poster=disabled)
-        post_thread(self.category, poster=disabled)
-
-        disabled.posts = 3
-        disabled.save()
-
-        build_active_posters_ranking()
-        ranking = get_active_posters_ranking()
-
-        self.assertEqual(ranking["users"], [self.user, other_user])
-        self.assertEqual(ranking["users_count"], 2)
-
-        self.assertEqual(ranking["users"][0].score, 2)
-        self.assertEqual(ranking["users"][1].score, 1)

+ 91 - 157
misago/users/tests/test_audittrail.py

@@ -1,186 +1,120 @@
 from datetime import timedelta
 
-from django.contrib.auth import get_user_model
+import pytest
 from django.utils import timezone
 
 from ..audittrail import create_audit_trail, create_user_audit_trail
 from ..models import AuditTrail
 from ..signals import remove_old_ips
-from ..test import UserTestCase, create_test_user
-
-User = get_user_model()
 
 USER_IP = "13.41.51.41"
 
 
-class MockRequest:
+class RequestMock:
     user_ip = USER_IP
 
     def __init__(self, user):
         self.user = user
 
 
-class CreateAuditTrailTests(UserTestCase):
-    def setUp(self):
-        super().setUp()
-
-        self.obj = create_test_user("OtherUser", "user@example.com")
-
-    def test_create_audit_require_model(self):
-        """create_audit_trail requires model instance"""
-        anonymous_user = self.get_anonymous_user()
-        request = MockRequest(anonymous_user)
-        with self.assertRaises(ValueError):
-            create_audit_trail(request, anonymous_user)
-        self.assertEqual(AuditTrail.objects.count(), 0)
-
-    def test_create_audit_trail_anonymous_user(self):
-        """create_audit_trail doesn't record anonymous users"""
-        user = self.get_anonymous_user()
-        request = MockRequest(user)
-        create_audit_trail(request, self.obj)
-        self.assertEqual(AuditTrail.objects.count(), 0)
-
-    def test_create_audit_trail(self):
-        """create_audit_trail creates new db record"""
-        user = self.get_authenticated_user()
-        request = MockRequest(user)
-        create_audit_trail(request, self.obj)
-        self.assertEqual(AuditTrail.objects.count(), 1)
-
-        audit_trail = user.audittrail_set.all()[0]
-        self.assertEqual(audit_trail.user, user)
-        self.assertEqual(audit_trail.ip_address, request.user_ip)
-        self.assertEqual(audit_trail.content_object, self.obj)
-
-    def test_delete_user_remove_audit_trail(self):
-        """audit trail is deleted together with user it belongs to"""
-        user = self.get_authenticated_user()
-        request = MockRequest(user)
-        create_audit_trail(request, self.obj)
-        self.assertEqual(AuditTrail.objects.count(), 1)
-
-        user.delete()
-        self.assertEqual(AuditTrail.objects.count(), 0)
-
-    def test_delete_obj_keep_audit_trail(self):
-        """audit trail is kept after with obj it points at is deleted"""
-        user = self.get_authenticated_user()
-        request = MockRequest(user)
-        create_audit_trail(request, self.obj)
-        self.assertEqual(AuditTrail.objects.count(), 1)
-
-        self.obj.delete()
-        self.assertEqual(AuditTrail.objects.count(), 1)
-
-        audit_trail = user.audittrail_set.all()[0]
-        self.assertEqual(audit_trail.user, user)
-        self.assertEqual(audit_trail.ip_address, request.user_ip)
-        self.assertIsNone(audit_trail.content_object)
-
-    def test_delete_audit_trail(self):
-        """audit trail deletion leaves other data untouched"""
-        user = self.get_authenticated_user()
-        request = MockRequest(user)
-        create_audit_trail(request, self.obj)
-        self.assertEqual(AuditTrail.objects.count(), 1)
-
-        audit_trail = user.audittrail_set.all()[0]
-        audit_trail.delete()
-
-        User.objects.get(id=user.id)
-        User.objects.get(id=self.obj.id)
-
-
-class CreateUserAuditTrailTests(UserTestCase):
-    def setUp(self):
-        super().setUp()
-
-        self.obj = create_test_user("OtherUser", "user@example.com")
-
-    def test_create_user_audit_require_model(self):
-        """create_user_audit_trail requires model instance"""
-        anonymous_user = self.get_anonymous_user()
-        with self.assertRaises(ValueError):
-            create_user_audit_trail(anonymous_user, USER_IP, anonymous_user)
-        self.assertEqual(AuditTrail.objects.count(), 0)
-
-    def test_create_user_audit_trail_anonymous_user(self):
-        """create_user_audit_trail doesn't record anonymous users"""
-        user = self.get_anonymous_user()
-        create_user_audit_trail(user, USER_IP, self.obj)
-        self.assertEqual(AuditTrail.objects.count(), 0)
-
-    def test_create_user_audit_trail(self):
-        """create_user_audit_trail creates new db record"""
-        user = self.get_authenticated_user()
-        create_user_audit_trail(user, USER_IP, self.obj)
-        self.assertEqual(AuditTrail.objects.count(), 1)
-
-        audit_trail = user.audittrail_set.all()[0]
-        self.assertEqual(audit_trail.user, user)
-        self.assertEqual(audit_trail.ip_address, USER_IP)
-        self.assertEqual(audit_trail.content_object, self.obj)
-
-    def test_delete_user_remove_audit_trail(self):
-        """audit trail is deleted together with user it belongs to"""
-        user = self.get_authenticated_user()
-        create_user_audit_trail(user, USER_IP, self.obj)
-        self.assertEqual(AuditTrail.objects.count(), 1)
-
-        user.delete()
-        self.assertEqual(AuditTrail.objects.count(), 0)
-
-    def test_delete_obj_keep_audit_trail(self):
-        """audit trail is kept after with obj it points at is deleted"""
-        user = self.get_authenticated_user()
-        create_user_audit_trail(user, USER_IP, self.obj)
-        self.assertEqual(AuditTrail.objects.count(), 1)
-
-        self.obj.delete()
-        self.assertEqual(AuditTrail.objects.count(), 1)
-
-        audit_trail = user.audittrail_set.all()[0]
-        self.assertEqual(audit_trail.user, user)
-        self.assertEqual(audit_trail.ip_address, USER_IP)
-        self.assertIsNone(audit_trail.content_object)
-
-    def test_delete_audit_trail(self):
-        """audit trail deletion leaves other data untouched"""
-        user = self.get_authenticated_user()
-        create_user_audit_trail(user, USER_IP, self.obj)
-        self.assertEqual(AuditTrail.objects.count(), 1)
+@pytest.fixture
+def request_mock(user):
+    return RequestMock(user)
+
+
+def test_audit_trail_creation_raises_value_error_if_target_is_not_model_instance(
+    request_mock, anonymous_user
+):
+    with pytest.raises(ValueError):
+        create_audit_trail(request_mock, anonymous_user)
+
+
+def test_audit_trail_is_not_created_for_anonymous_users(anonymous_user, user):
+    request_mock = RequestMock(anonymous_user)
+    create_audit_trail(request_mock, user)
+    assert not AuditTrail.objects.exists()
+
+
+def test_audit_trail_is_created(request_mock, other_user):
+    assert create_audit_trail(request_mock, other_user)
+    assert AuditTrail.objects.exists()
+
+
+def test_audit_trail_is_created_with_request_data(request_mock, user, other_user):
+    audit_trail = create_audit_trail(request_mock, other_user)
+
+    assert audit_trail.user == user
+    assert audit_trail.ip_address == USER_IP
+
+
+def test_audit_trail_is_created_with_generic_relation_to_target(
+    request_mock, user, other_user
+):
+    audit_trail = create_audit_trail(request_mock, other_user)
+    assert audit_trail.content_object == other_user
+
+
+def test_audit_trail_is_deleted_together_with_user(request_mock, user, other_user):
+    audit_trail = create_audit_trail(request_mock, other_user)
+    user.delete(anonymous_username="Deleted")
+    with pytest.raises(AuditTrail.DoesNotExist):
+        audit_trail.refresh_from_db()
+
+
+def test_audit_trail_is_kept_after_its_target_is_deleted(request_mock, other_user):
+    audit_trail = create_audit_trail(request_mock, other_user)
+    other_user.delete(anonymous_username="Deleted")
+    audit_trail.refresh_from_db()
+
+
+def test_deleting_audit_trail_leaves_user(request_mock, user, other_user):
+    audit_trail = create_audit_trail(request_mock, other_user)
+    audit_trail.delete()
+    user.refresh_from_db()
+
+
+def test_deleting_audit_trail_leaves_target(request_mock, user, other_user):
+    audit_trail = create_audit_trail(request_mock, other_user)
+    audit_trail.delete()
+    other_user.refresh_from_db()
+
+
+def test_audit_trail_can_be_created_without_request(user, other_user):
+    assert create_user_audit_trail(user, USER_IP, other_user)
+    assert AuditTrail.objects.exists()
 
-        audit_trail = user.audittrail_set.all()[0]
-        audit_trail.delete()
 
-        User.objects.get(id=user.id)
-        User.objects.get(id=self.obj.id)
+def test_audit_trail_creation_without_request_raises_value_error_if_target_is_not_model(
+    user, anonymous_user
+):
+    with pytest.raises(ValueError):
+        create_user_audit_trail(user, USER_IP, anonymous_user)
 
 
-class RemoveOldAuditTrailsTest(UserTestCase):
-    def setUp(self):
-        super().setUp()
+def test_audit_trail_without_request_is_created_with_explicit_data(user, other_user):
+    audit_trail = create_user_audit_trail(user, USER_IP, other_user)
 
-        self.obj = create_test_user("OtherUser", "user@example.com")
+    assert audit_trail.user == user
+    assert audit_trail.ip_address == USER_IP
 
-    def test_recent_audit_trail_is_kept(self):
-        """remove_old_ips keeps recent audit trails"""
-        user = self.get_authenticated_user()
-        create_user_audit_trail(user, USER_IP, self.obj)
 
-        remove_old_ips.send(None)
+def test_audit_trail_without_request_is_created_with_generic_relation_to_target(
+    user, other_user
+):
+    audit_trail = create_user_audit_trail(user, USER_IP, other_user)
+    assert audit_trail.content_object == other_user
 
-        self.assertEqual(user.audittrail_set.count(), 1)
 
-    def test_old_audit_trail_is_removed(self):
-        """remove_old_ips removes old audit trails"""
-        user = self.get_authenticated_user()
-        audit_trail = create_user_audit_trail(user, USER_IP, self.obj)
+def test_recent_audit_trail_is_not_deleted_on_signal(user, other_user):
+    create_user_audit_trail(user, USER_IP, other_user)
+    remove_old_ips.send(None, ip_storage_time=1)
+    assert user.audittrail_set.exists()
 
-        audit_trail.created_on = timezone.now() - timedelta(days=50)
-        audit_trail.save()
 
-        remove_old_ips.send(None)
+def test_old_audit_trail_is_deleted_on_signal(user, other_user):
+    audit_trail = create_user_audit_trail(user, USER_IP, other_user)
+    audit_trail.created_on = timezone.now() - timedelta(days=6)
+    audit_trail.save()
 
-        self.assertEqual(user.audittrail_set.count(), 0)
+    remove_old_ips.send(None, ip_storage_time=5)
+    assert not user.audittrail_set.exists()

+ 11 - 1
misago/users/tests/test_datadownloads.py

@@ -1,6 +1,8 @@
 import os
+from datetime import timedelta
 
 from django.core.files import File
+from django.utils import timezone
 
 from ...categories.models import Category
 from ...threads.models import AttachmentType
@@ -15,6 +17,7 @@ from ..datadownloads import (
 from ..models import DataDownload
 from ..test import AuthenticatedUserTestCase
 
+EXPIRATION = 4
 TESTFILES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "testfiles")
 TEST_FILE_PATH = os.path.join(TESTFILES_DIR, "avatar.png")
 
@@ -65,7 +68,7 @@ class PrepareUserDataDownload(AuthenticatedUserTestCase):
         self.download = request_user_data_download(self.user)
 
     def assert_download_is_valid(self):
-        result = prepare_user_data_download(self.download)
+        result = prepare_user_data_download(self.download, EXPIRATION)
         self.assertTrue(result)
 
         self.download.refresh_from_db()
@@ -75,6 +78,13 @@ class PrepareUserDataDownload(AuthenticatedUserTestCase):
         """function creates data download for basic user account"""
         self.assert_download_is_valid()
 
+    def test_data_download_is_prepared_with_expiration_date(self):
+        """function creates data download with specified expiration date"""
+        expires_on = timezone.now() + timedelta(hours=EXPIRATION)
+        prepare_user_data_download(self.download, EXPIRATION)
+        self.download.refresh_from_db
+        self.assertGreater(self.download.expires_on, expires_on)
+
     def test_prepare_download_with_profle_fields(self):
         """function creates data download for user with profile fields"""
         self.user.profile_fields = {"real_name": "Bob Boberthon!"}

+ 14 - 13
misago/users/tests/test_deleteinactiveusers.py

@@ -3,9 +3,10 @@ from io import StringIO
 
 from django.contrib.auth import get_user_model
 from django.core.management import call_command
-from django.test import TestCase, override_settings
+from django.test import TestCase
 from django.utils import timezone
 
+from ...conf.test import override_dynamic_settings
 from ..management.commands import deleteinactiveusers
 from ..test import create_test_user
 
@@ -16,7 +17,7 @@ class DeleteInactiveUsersTests(TestCase):
     def setUp(self):
         self.user = create_test_user("User", "user@example.com")
 
-    @override_settings(MISAGO_DELETE_NEW_INACTIVE_USERS_OLDER_THAN_DAYS=2)
+    @override_dynamic_settings(new_inactive_accounts_delete=2)
     def test_delete_user_activation_user(self):
         """deletes user that didn't activate their account within required time"""
         self.user.joined_on = timezone.now() - timedelta(days=2)
@@ -27,12 +28,12 @@ class DeleteInactiveUsersTests(TestCase):
         call_command(deleteinactiveusers.Command(), stdout=out)
         command_output = out.getvalue().splitlines()[0].strip()
 
-        self.assertEqual(command_output, "Deleted users: 1")
+        self.assertEqual(command_output, "Deleted inactive user accounts: 1")
 
         with self.assertRaises(User.DoesNotExist):
             self.user.refresh_from_db()
 
-    @override_settings(MISAGO_DELETE_NEW_INACTIVE_USERS_OLDER_THAN_DAYS=2)
+    @override_dynamic_settings(new_inactive_accounts_delete=2)
     def test_delete_user_activation_admin(self):
         """deletes user that wasn't activated by admin within required time"""
         self.user.joined_on = timezone.now() - timedelta(days=2)
@@ -43,12 +44,12 @@ class DeleteInactiveUsersTests(TestCase):
         call_command(deleteinactiveusers.Command(), stdout=out)
         command_output = out.getvalue().splitlines()[0].strip()
 
-        self.assertEqual(command_output, "Deleted users: 1")
+        self.assertEqual(command_output, "Deleted inactive user accounts: 1")
 
         with self.assertRaises(User.DoesNotExist):
             self.user.refresh_from_db()
 
-    @override_settings(MISAGO_DELETE_NEW_INACTIVE_USERS_OLDER_THAN_DAYS=2)
+    @override_dynamic_settings(new_inactive_accounts_delete=2)
     def test_skip_new_user_activation_user(self):
         """skips inactive user that is too new"""
         self.user.joined_on = timezone.now() - timedelta(days=1)
@@ -59,11 +60,11 @@ class DeleteInactiveUsersTests(TestCase):
         call_command(deleteinactiveusers.Command(), stdout=out)
         command_output = out.getvalue().splitlines()[0].strip()
 
-        self.assertEqual(command_output, "Deleted users: 0")
+        self.assertEqual(command_output, "Deleted inactive user accounts: 0")
 
         self.user.refresh_from_db()
 
-    @override_settings(MISAGO_DELETE_NEW_INACTIVE_USERS_OLDER_THAN_DAYS=2)
+    @override_dynamic_settings(new_inactive_accounts_delete=2)
     def test_skip_new_user_activation_admin(self):
         """skips admin-activated user that is too new"""
         self.user.joined_on = timezone.now() - timedelta(days=1)
@@ -74,11 +75,11 @@ class DeleteInactiveUsersTests(TestCase):
         call_command(deleteinactiveusers.Command(), stdout=out)
         command_output = out.getvalue().splitlines()[0].strip()
 
-        self.assertEqual(command_output, "Deleted users: 0")
+        self.assertEqual(command_output, "Deleted inactive user accounts: 0")
 
         self.user.refresh_from_db()
 
-    @override_settings(MISAGO_DELETE_NEW_INACTIVE_USERS_OLDER_THAN_DAYS=2)
+    @override_dynamic_settings(new_inactive_accounts_delete=2)
     def test_skip_active_user(self):
         """skips active user"""
         self.user.joined_on = timezone.now() - timedelta(days=1)
@@ -88,11 +89,11 @@ class DeleteInactiveUsersTests(TestCase):
         call_command(deleteinactiveusers.Command(), stdout=out)
         command_output = out.getvalue().splitlines()[0].strip()
 
-        self.assertEqual(command_output, "Deleted users: 0")
+        self.assertEqual(command_output, "Deleted inactive user accounts: 0")
 
         self.user.refresh_from_db()
 
-    @override_settings(MISAGO_DELETE_NEW_INACTIVE_USERS_OLDER_THAN_DAYS=0)
+    @override_dynamic_settings(new_inactive_accounts_delete=0)
     def test_delete_inactive_is_disabled(self):
         """skips active user"""
         self.user.joined_on = timezone.now() - timedelta(days=1)
@@ -105,7 +106,7 @@ class DeleteInactiveUsersTests(TestCase):
 
         self.assertEqual(
             command_output,
-            "Automatic deletion of inactive users is currently disabled.",
+            "Automatic deletion of inactive user accounts is currently disabled.",
         )
 
         self.user.refresh_from_db()

+ 3 - 2
misago/users/tests/test_deletemarkedusers.py

@@ -2,8 +2,9 @@ from io import StringIO
 
 from django.contrib.auth import get_user_model
 from django.core.management import call_command
-from django.test import TestCase, override_settings
+from django.test import TestCase
 
+from ...conf.test import override_dynamic_settings
 from ..management.commands import deletemarkedusers
 from ..test import create_test_user
 
@@ -26,7 +27,7 @@ class DeleteMarkedUsersTests(TestCase):
         with self.assertRaises(User.DoesNotExist):
             self.user.refresh_from_db()
 
-    @override_settings(MISAGO_ENABLE_DELETE_OWN_ACCOUNT=False)
+    @override_dynamic_settings(allow_delete_own_account=False)
     def test_delete_disabled(self):
         """deletion respects user decision even if configuration has changed"""
         out = StringIO()

+ 2 - 2
misago/users/tests/test_prepareuserdatadownloads.py

@@ -11,14 +11,14 @@ from ..test import AuthenticatedUserTestCase
 
 
 class PrepareUserDataDownloadsTests(AuthenticatedUserTestCase):
+    @override_dynamic_settings(forum_address="http://test.com/")
     def test_process_pending_data_download(self):
         """management command processes pending data download"""
         data_download = request_user_data_download(self.user)
         self.assertEqual(data_download.status, DataDownload.STATUS_PENDING)
 
         out = StringIO()
-        with override_dynamic_settings(forum_address="http://test.com/"):
-            call_command(prepareuserdatadownloads.Command(), stdout=out)
+        call_command(prepareuserdatadownloads.Command(), stdout=out)
 
         command_output = out.getvalue().splitlines()[0].strip()
         self.assertEqual(command_output, "Data downloads prepared: 1")

+ 6 - 6
misago/users/tests/test_remove_old_ips_command.py

@@ -3,9 +3,9 @@ from io import StringIO
 
 import pytest
 from django.core.management import call_command
-from django.test import override_settings
 from django.utils import timezone
 
+from ...conf.test import override_dynamic_settings
 from ..management.commands import removeoldips
 
 IP_STORE_TIME = 2
@@ -32,14 +32,14 @@ def test_recent_user_joined_ip_is_not_removed_by_command(user_with_ip):
     assert user_with_ip.joined_from_ip
 
 
-@override_settings(MISAGO_IP_STORE_TIME=IP_STORE_TIME)
+@override_dynamic_settings(ip_storage_time=IP_STORE_TIME)
 def test_old_user_joined_ip_is_removed_by_command(user_with_old_ip):
     call_command(removeoldips.Command(), stdout=StringIO())
     user_with_old_ip.refresh_from_db()
     assert user_with_old_ip.joined_from_ip is None
 
 
-@override_settings(MISAGO_IP_STORE_TIME=None)
+@override_dynamic_settings(ip_storage_time=None)
 def test_old_user_joined_ip_is_not_removed_by_command_if_removal_is_disabled(
     user_with_old_ip
 ):
@@ -48,7 +48,7 @@ def test_old_user_joined_ip_is_not_removed_by_command_if_removal_is_disabled(
     assert user_with_old_ip.joined_from_ip
 
 
-@override_settings(MISAGO_IP_STORE_TIME=IP_STORE_TIME)
+@override_dynamic_settings(ip_storage_time=IP_STORE_TIME)
 def test_command_displays_message_if_old_ip_removal_is_enabled(db):
     stdout = StringIO()
     call_command(removeoldips.Command(), stdout=stdout)
@@ -57,8 +57,8 @@ def test_command_displays_message_if_old_ip_removal_is_enabled(db):
     assert command_output == "IP addresses older than 2 days have been removed."
 
 
-@override_settings(MISAGO_IP_STORE_TIME=None)
-def test_command_displays_message_if_old_ip_removal_is_disabled():
+@override_dynamic_settings(ip_storage_time=None)
+def test_command_displays_message_if_old_ip_removal_is_disabled(db):
     stdout = StringIO()
     call_command(removeoldips.Command(), stdout=stdout)
 

+ 1 - 1
misago/users/tests/test_search.py

@@ -69,7 +69,7 @@ class SearchApiTests(AuthenticatedUserTestCase):
                 self.assertEqual(len(results), 1)
                 self.assertEqual(results[0]["id"], self.user.id)
 
-    def test_tail_match(self):
+    def test_orphans_match(self):
         """api handles last three chars match query"""
         response = self.client.get("%s?q=%s" % (self.api_link, self.user.username[-3:]))
         self.assertEqual(response.status_code, 200)

+ 98 - 0
misago/users/tests/test_stop_forum_spam_validator.py

@@ -0,0 +1,98 @@
+import json
+from unittest.mock import Mock
+
+import pytest
+from django.forms import ValidationError
+from requests.exceptions import Timeout
+
+from ...conf.test import override_dynamic_settings
+from ..validators import validate_with_sfs
+
+cleaned_data = {"email": "test@test.com"}
+
+
+@pytest.fixture
+def request_mock(dynamic_settings):
+    return Mock(settings=dynamic_settings, user_ip="127.0.0.1")
+
+
+@pytest.fixture
+def api_mock(mocker):
+    return mocker.patch(
+        "misago.users.validators.requests",
+        get=Mock(
+            return_value=Mock(
+                content=json.dumps(
+                    {"email": {"confidence": 55}, "ip": {"confidence": 45}}
+                )
+            )
+        ),
+    )
+
+
+@override_dynamic_settings(enable_stop_forum_spam=False)
+def test_api_is_not_called_if_sfs_is_disabled(api_mock, request_mock):
+    validate_with_sfs(request_mock, cleaned_data, None)
+    api_mock.get.assert_not_called()
+
+
+@override_dynamic_settings(enable_stop_forum_spam=True)
+def test_api_is_not_called_if_email_is_not_available(api_mock, request_mock):
+    validate_with_sfs(request_mock, {}, None)
+    api_mock.get.assert_not_called()
+
+
+@override_dynamic_settings(enable_stop_forum_spam=True, stop_forum_spam_confidence=90)
+def test_api_is_called_if_sfs_is_enabled_and_email_is_provided(api_mock, request_mock):
+    validate_with_sfs(request_mock, cleaned_data, None)
+    api_mock.get.assert_called_once()
+
+
+@override_dynamic_settings(enable_stop_forum_spam=True, stop_forum_spam_confidence=50)
+def test_validator_raises_error_if_ip_score_is_greater_than_confidence(
+    api_mock, request_mock
+):
+    with pytest.raises(ValidationError):
+        validate_with_sfs(request_mock, cleaned_data, None)
+
+
+@override_dynamic_settings(enable_stop_forum_spam=True, stop_forum_spam_confidence=52)
+def test_validator_raises_error_if_email_score_is_greater_than_confidence(
+    api_mock, request_mock
+):
+    with pytest.raises(ValidationError):
+        validate_with_sfs(request_mock, cleaned_data, None)
+
+
+@override_dynamic_settings(enable_stop_forum_spam=True)
+def test_validator_handles_api_error(mocker, request_mock):
+    failing_api_mock = mocker.patch(
+        "misago.users.validators.requests",
+        get=Mock(return_value=Mock(raise_for_status=Mock(side_effect=Timeout()))),
+    )
+
+    validate_with_sfs(request_mock, cleaned_data, None)
+    failing_api_mock.get.assert_called_once()
+
+
+@override_dynamic_settings(enable_stop_forum_spam=True)
+def test_validator_logs_api_error(mocker, request_mock):
+    failing_api_mock = mocker.patch(
+        "misago.users.validators.requests",
+        get=Mock(return_value=Mock(raise_for_status=Mock(side_effect=Timeout()))),
+    )
+    logger_mock = mocker.patch("misago.users.validators.logger", exception=Mock())
+
+    validate_with_sfs(request_mock, cleaned_data, None)
+    failing_api_mock.get.assert_called_once()
+    logger_mock.exception.assert_called_once()
+
+
+@override_dynamic_settings(enable_stop_forum_spam=True)
+def test_validator_handles_malformed_api_response(mocker, request_mock):
+    failing_api_mock = mocker.patch(
+        "misago.users.validators.requests", get=Mock(return_value=Mock(content="{}"))
+    )
+
+    validate_with_sfs(request_mock, cleaned_data, None)
+    failing_api_mock.get.assert_called_once()

+ 77 - 0
misago/users/tests/test_top_posters_ranking.py

@@ -0,0 +1,77 @@
+from datetime import timedelta
+
+from django.utils import timezone
+
+from ...conf.test import override_dynamic_settings
+from ...threads.test import reply_thread
+from ..activepostersranking import (
+    build_active_posters_ranking,
+    get_active_posters_ranking,
+)
+from ..models import ActivityRanking
+from ..test import create_test_user
+
+
+def test_ranking_is_emptied_if_no_users_exist(post):
+    assert not build_active_posters_ranking()
+
+
+def test_ranking_is_emptied_if_no_posts_exist(user):
+    assert not build_active_posters_ranking()
+
+
+@override_dynamic_settings(top_posters_ranking_length=5)
+def test_recent_post_by_user_counts_to_ranking(thread, user):
+    reply_thread(thread, poster=user)
+    assert build_active_posters_ranking()
+
+
+@override_dynamic_settings(top_posters_ranking_length=5)
+def test_recent_post_by_removed_user_doesnt_count_to_ranking(thread):
+    reply_thread(thread)
+    assert not build_active_posters_ranking()
+
+
+@override_dynamic_settings(top_posters_ranking_length=5)
+def test_old_post_by_user_doesnt_count_to_ranking(thread, user):
+    reply_thread(thread, poster=user, posted_on=timezone.now() - timedelta(days=6))
+    assert not build_active_posters_ranking()
+
+
+@override_dynamic_settings(top_posters_ranking_size=2)
+def test_ranking_size_is_limited(thread):
+    for i in range(3):
+        user = create_test_user("User%s" % i, "user%s@example.com" % i)
+        reply_thread(thread, poster=user)
+    assert len(build_active_posters_ranking()) == 2
+
+
+@override_dynamic_settings(top_posters_ranking_size=2)
+def test_old_ranking_is_removed_during_build(user):
+    ActivityRanking.objects.create(user=user, score=1)
+    build_active_posters_ranking()
+    assert not ActivityRanking.objects.exists()
+
+
+def test_empty_ranking_is_returned_from_db(db):
+    assert get_active_posters_ranking() == {"users": [], "users_count": 0}
+
+
+def test_ranking_is_returned_from_db(user):
+    ActivityRanking.objects.create(user=user, score=1)
+    assert get_active_posters_ranking() == {"users": [user], "users_count": 1}
+
+
+def test_ranked_user_is_annotated_with_score(user):
+    ActivityRanking.objects.create(user=user, score=1)
+    ranked_user = get_active_posters_ranking()["users"][0]
+    assert ranked_user.score == 1
+
+
+def test_ranked_users_are_ordered_by_score(user, other_user):
+    ActivityRanking.objects.create(user=user, score=1)
+    ActivityRanking.objects.create(user=other_user, score=2)
+    assert get_active_posters_ranking() == {
+        "users": [other_user, user],
+        "users_count": 2,
+    }

+ 10 - 10
misago/users/tests/test_user_changeemail_api.py

@@ -73,15 +73,15 @@ class UserChangeEmailTests(AuthenticatedUserTestCase):
             response.json(), {"new_email": ["This e-mail address is not available."]}
         )
 
+    @override_dynamic_settings(forum_address="http://test.com/")
     def test_change_email(self):
         """api allows users to change their e-mail addresses"""
         new_email = "new@email.com"
 
-        with override_dynamic_settings(forum_address="http://test.com/"):
-            response = self.client.post(
-                self.link, data={"new_email": new_email, "password": self.USER_PASSWORD}
-            )
-            self.assertEqual(response.status_code, 200)
+        response = self.client.post(
+            self.link, data={"new_email": new_email, "password": self.USER_PASSWORD}
+        )
+        self.assertEqual(response.status_code, 200)
 
         self.assertIn("Confirm e-mail change", mail.outbox[0].subject)
         for line in [l.strip() for l in mail.outbox[0].body.splitlines()]:
@@ -100,6 +100,7 @@ class UserChangeEmailTests(AuthenticatedUserTestCase):
         self.reload_user()
         self.assertEqual(self.user.email, new_email)
 
+    @override_dynamic_settings(forum_address="http://test.com/")
     def test_change_email_user_password_whitespace(self):
         """api supports users with whitespace around their passwords"""
         user_password = " old password "
@@ -110,11 +111,10 @@ class UserChangeEmailTests(AuthenticatedUserTestCase):
 
         self.login_user(self.user)
 
-        with override_dynamic_settings(forum_address="http://test.com/"):
-            response = self.client.post(
-                self.link, data={"new_email": new_email, "password": user_password}
-            )
-            self.assertEqual(response.status_code, 200)
+        response = self.client.post(
+            self.link, data={"new_email": new_email, "password": user_password}
+        )
+        self.assertEqual(response.status_code, 200)
 
         self.assertIn("Confirm e-mail change", mail.outbox[0].subject)
         for line in [l.strip() for l in mail.outbox[0].body.splitlines()]:

+ 11 - 11
misago/users/tests/test_user_changepassword_api.py

@@ -68,16 +68,16 @@ class UserChangePasswordTests(AuthenticatedUserTestCase):
             },
         )
 
+    @override_dynamic_settings(forum_address="http://test.com/")
     def test_change_password(self):
         """api allows users to change their passwords"""
         new_password = "N3wP@55w0rd"
 
-        with override_dynamic_settings(forum_address="http://test.com/"):
-            response = self.client.post(
-                self.link,
-                data={"new_password": new_password, "password": self.USER_PASSWORD},
-            )
-            self.assertEqual(response.status_code, 200)
+        response = self.client.post(
+            self.link,
+            data={"new_password": new_password, "password": self.USER_PASSWORD},
+        )
+        self.assertEqual(response.status_code, 200)
 
         self.assertIn("Confirm password change", mail.outbox[0].subject)
         for line in [l.strip() for l in mail.outbox[0].body.splitlines()]:
@@ -96,6 +96,7 @@ class UserChangePasswordTests(AuthenticatedUserTestCase):
         self.reload_user()
         self.assertTrue(self.user.check_password(new_password))
 
+    @override_dynamic_settings(forum_address="http://test.com/")
     def test_change_password_with_whitespaces(self):
         """api handles users with whitespaces around their passwords"""
         old_password = " old password "
@@ -106,11 +107,10 @@ class UserChangePasswordTests(AuthenticatedUserTestCase):
 
         self.login_user(self.user)
 
-        with override_dynamic_settings(forum_address="http://test.com/"):
-            response = self.client.post(
-                self.link, data={"new_password": new_password, "password": old_password}
-            )
-            self.assertEqual(response.status_code, 200)
+        response = self.client.post(
+            self.link, data={"new_password": new_password, "password": old_password}
+        )
+        self.assertEqual(response.status_code, 200)
 
         self.assertIn("Confirm password change", mail.outbox[0].subject)
         for line in [l.strip() for l in mail.outbox[0].body.splitlines()]:

+ 7 - 7
misago/users/tests/test_user_model.py

@@ -10,10 +10,10 @@ from ..models import Avatar, DataDownload, User
 from ..utils import hash_email
 
 
-def test_username_and_slug_is_removed_by_anonymization(user):
-    user.anonymize_data()
-    assert user.username == settings.MISAGO_ANONYMOUS_USERNAME
-    assert user.slug == slugify(settings.MISAGO_ANONYMOUS_USERNAME)
+def test_username_and_slug_is_anonymized(user):
+    user.anonymize_data(anonymous_username="Deleted")
+    assert user.username == "Deleted"
+    assert user.slug == slugify("Deleted")
 
 
 def test_user_avatar_files_are_deleted_during_user_deletion(user):
@@ -28,7 +28,7 @@ def test_user_avatar_files_are_deleted_during_user_deletion(user):
         user_avatars.append(avatar)
     assert user_avatars
 
-    user.delete()
+    user.delete(anonymous_username="Deleted")
 
     for removed_avatar in user_avatars:
         avatar_path = Path(removed_avatar.image.path)
@@ -85,7 +85,7 @@ def test_marking_user_for_deletion_deactivates_their_account_in_db(user):
 
 def test_user_data_downloads_are_removed_by_anonymization(user):
     data_download = request_user_data_download(user)
-    user.anonymize_data()
+    user.anonymize_data(anonymous_username="Deleted")
 
     with pytest.raises(DataDownload.DoesNotExist):
         data_download.refresh_from_db()
@@ -93,7 +93,7 @@ def test_user_data_downloads_are_removed_by_anonymization(user):
 
 def test_deleting_user_also_deletes_their_data_downloads(user):
     data_download = request_user_data_download(user)
-    user.delete()
+    user.delete(anonymous_username="Deleted")
 
     with pytest.raises(DataDownload.DoesNotExist):
         data_download.refresh_from_db()

+ 3 - 4
misago/users/tests/test_user_requestdatadownload_api.py

@@ -1,5 +1,4 @@
-from django.test.utils import override_settings
-
+from ...conf.test import override_dynamic_settings
 from ..datadownloads import request_user_data_download
 from ..test import AuthenticatedUserTestCase
 
@@ -31,7 +30,7 @@ class UserRequestDataDownload(AuthenticatedUserTestCase):
             {"detail": "You can't request data downloads for other users."},
         )
 
-    @override_settings(MISAGO_ENABLE_DOWNLOAD_OWN_DATA=False)
+    @override_dynamic_settings(allow_data_downloads=False)
     def test_request_download_disabled(self):
         """request to api fails if own data downloads are disabled"""
         response = self.client.post(self.link)
@@ -48,7 +47,7 @@ class UserRequestDataDownload(AuthenticatedUserTestCase):
             response.json(),
             {
                 "detail": (
-                    "You can't have more than one data download request at single time."
+                    "You can't have more than one data download request at a single time."
                 )
             },
         )

+ 6 - 2
misago/users/tests/test_users_api.py

@@ -2,11 +2,11 @@ import json
 from datetime import timedelta
 
 from django.contrib.auth import get_user_model
-from django.test import override_settings
 from django.urls import reverse
 
 from ...acl.test import patch_user_acl
 from ...categories.models import Category
+from ...conf.test import override_dynamic_settings
 from ...threads.models import Post, Thread
 from ...threads.test import post_thread
 from ..activepostersranking import build_active_posters_ranking
@@ -463,7 +463,7 @@ class UserDeleteOwnAccountTests(AuthenticatedUserTestCase):
         super().setUp()
         self.api_link = "/api/users/%s/delete-own-account/" % self.user.pk
 
-    @override_settings(MISAGO_ENABLE_DELETE_OWN_ACCOUNT=False)
+    @override_dynamic_settings(allow_delete_own_account=False)
     def test_delete_own_account_feature_disabled(self):
         """
         raises 403 error when attempting to delete own account but feature is disabled
@@ -477,6 +477,7 @@ class UserDeleteOwnAccountTests(AuthenticatedUserTestCase):
         self.assertTrue(self.user.is_active)
         self.assertFalse(self.user.is_deleting_account)
 
+    @override_dynamic_settings(allow_delete_own_account=True)
     def test_delete_own_account_is_staff(self):
         """raises 403 error when attempting to delete own account as admin"""
         self.user.is_staff = True
@@ -497,6 +498,7 @@ class UserDeleteOwnAccountTests(AuthenticatedUserTestCase):
         self.assertTrue(self.user.is_active)
         self.assertFalse(self.user.is_deleting_account)
 
+    @override_dynamic_settings(allow_delete_own_account=True)
     def test_delete_own_account_is_superuser(self):
         """raises 403 error when attempting to delete own account as superadmin"""
         self.user.is_superuser = True
@@ -517,6 +519,7 @@ class UserDeleteOwnAccountTests(AuthenticatedUserTestCase):
         self.assertTrue(self.user.is_active)
         self.assertFalse(self.user.is_deleting_account)
 
+    @override_dynamic_settings(allow_delete_own_account=True)
     def test_delete_own_account_invalid_password(self):
         """
         raises 400 error when attempting to delete own account with invalid password
@@ -531,6 +534,7 @@ class UserDeleteOwnAccountTests(AuthenticatedUserTestCase):
         self.assertTrue(self.user.is_active)
         self.assertFalse(self.user.is_deleting_account)
 
+    @override_dynamic_settings(allow_delete_own_account=True)
     def test_delete_own_account(self):
         """deactivates account and marks it for deletion"""
         response = self.client.post(self.api_link, {"password": self.USER_PASSWORD})

+ 22 - 15
misago/users/validators.py

@@ -1,4 +1,5 @@
 import json
+import logging
 import re
 
 import requests
@@ -9,12 +10,15 @@ from django.utils.encoding import force_str
 from django.utils.module_loading import import_string
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import ngettext
+from requests.exceptions import RequestException
 
 from ..conf import settings
 from .bans import get_email_ban, get_username_ban
 
 USERNAME_RE = re.compile(r"^[0-9a-z]+$", re.IGNORECASE)
 
+logger = logging.getLogger("misago.users.validators")
+
 User = get_user_model()
 
 
@@ -107,26 +111,29 @@ SFS_API_URL = (
 
 
 def validate_with_sfs(request, cleaned_data, add_error):
-    if settings.MISAGO_USE_STOP_FORUM_SPAM and cleaned_data.get("email"):
-        _real_validate_with_sfs(request.user_ip, cleaned_data["email"])
-
+    if request.settings.enable_stop_forum_spam and cleaned_data.get("email"):
+        try:
+            _real_validate_with_sfs(
+                request.user_ip,
+                cleaned_data["email"],
+                request.settings.stop_forum_spam_confidence,
+            )
+        except RequestException as error:
+            logger.exception(error, exc_info=error)
 
-def _real_validate_with_sfs(ip, email):
-    try:
-        r = requests.get(SFS_API_URL % {"email": email, "ip": ip}, timeout=5)
 
-        r.raise_for_status()
+def _real_validate_with_sfs(ip, email, confidence):
+    r = requests.get(SFS_API_URL % {"email": email, "ip": ip}, timeout=5)
+    r.raise_for_status()
 
-        api_response = json.loads(force_str(r.content))
-        ip_score = api_response.get("ip", {}).get("confidence", 0)
-        email_score = api_response.get("email", {}).get("confidence", 0)
+    api_response = json.loads(force_str(r.content))
+    ip_score = api_response.get("ip", {}).get("confidence", 0)
+    email_score = api_response.get("email", {}).get("confidence", 0)
 
-        api_score = max((ip_score, email_score))
+    api_score = max((ip_score, email_score))
 
-        if api_score > settings.MISAGO_STOP_FORUM_SPAM_MIN_CONFIDENCE:
-            raise ValidationError(_("Data entered was found in spammers database."))
-    except requests.exceptions.RequestException:
-        pass  # todo: log those somewhere
+    if api_score > confidence:
+        raise ValidationError(_("Data entered was found in spammers database."))
 
 
 def validate_gmail_email(request, cleaned_data, add_error):

+ 1 - 2
misago/users/viewmodels/activeposters.py

@@ -1,4 +1,3 @@
-from ...conf import settings
 from ..activepostersranking import get_active_posters_ranking
 from ..online.utils import make_users_status_aware
 from ..serializers import UserCardSerializer
@@ -10,7 +9,7 @@ class ActivePosters:
         make_users_status_aware(request, ranking["users"], fetch_state=True)
 
         self.count = ranking["users_count"]
-        self.tracked_period = settings.MISAGO_RANKING_LENGTH
+        self.tracked_period = request.settings.top_posters_ranking_length
         self.users = ranking["users"]
 
     def get_frontend_context(self):

+ 6 - 2
misago/users/viewmodels/followers.py

@@ -1,6 +1,5 @@
 from django.http import Http404
 
-from ...conf import settings
 from ...core.shortcuts import paginate, pagination_dict
 from ..online.utils import make_users_status_aware
 from ..serializers import UserCardSerializer
@@ -20,7 +19,12 @@ class Followers:
             else:
                 raise Http404()
 
-        list_page = paginate(queryset, page, settings.MISAGO_USERS_PER_PAGE, 4)
+        list_page = paginate(
+            queryset,
+            page,
+            request.settings.users_per_page,
+            request.settings.users_per_page_orphans,
+        )
         make_users_status_aware(request, list_page.object_list)
 
         self.users = list_page.object_list

+ 6 - 2
misago/users/viewmodels/rankusers.py

@@ -1,4 +1,3 @@
-from ...conf import settings
 from ...core.shortcuts import paginate, pagination_dict
 from ..online.utils import make_users_status_aware
 from ..serializers import UserCardSerializer
@@ -13,7 +12,12 @@ class RankUsers:
         if not request.user.is_staff:
             queryset = queryset.filter(is_active=True)
 
-        list_page = paginate(queryset, page, settings.MISAGO_USERS_PER_PAGE, 4)
+        list_page = paginate(
+            queryset,
+            page,
+            request.settings.users_per_page,
+            request.settings.users_per_page_orphans,
+        )
         make_users_status_aware(request, list_page.object_list)
 
         self.users = list_page.object_list

+ 1 - 2
misago/users/viewmodels/threads.py

@@ -2,7 +2,6 @@ from django.core.paginator import EmptyPage, InvalidPage
 from django.http import Http404
 
 from ...acl.objectacl import add_acl_to_obj
-from ...conf import settings
 from ...core.cursorpagination import get_page
 from ...core.shortcuts import paginate, pagination_dict
 from ...threads.permissions import exclude_invisible_threads
@@ -28,7 +27,7 @@ class UserThreads:
 
         try:
             list_page = get_page(
-                posts_queryset, "-id", settings.MISAGO_POSTS_PER_PAGE, start
+                posts_queryset, "-id", request.settings.posts_per_page, start
             )
         except (EmptyPage, InvalidPage):
             raise Http404()